diff --git a/CHANGELOG.md b/CHANGELOG.md index c8d8fabf9761719a0a7eb315f1fad4b0d600ccf9..a82d58a262ce39eabd06187a0df67f3ba8209d2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,14 @@ 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 Go code runtime for custom functions and authoritative match handlers. +- Lua runtime token generator function now returns a second value representing the token's expiry. +- Add local cache for in-memory storage to the Lua runtime. +### Fixed +- Correctly merge new friend records when importing from Facebook. +- Log correct registered hook names at startup. ## [2.0.3] - 2018-08-10 ### Added diff --git a/build/Dockerfile b/build/Dockerfile index 1e17d89ca45053c564f8468fe5a736a887827349..c704673dd28ea7e2ccd6004ddfe2edec1b1270a2 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -1,3 +1,21 @@ +# docker build . --rm --build-arg version=2.0.4 --build-arg commit=master -t heroiclabs/nakama:2.0.4 + +FROM golang:1.10-alpine3.7 as builder + +ARG commit + +RUN apk --no-cache add ca-certificates gcc musl-dev git + +WORKDIR /go/src/github.com/heroiclabs/ +RUN git config --global advice.detachedHead false && \ + git clone -q -n https://github.com/heroiclabs/nakama + +WORKDIR /go/src/github.com/heroiclabs/nakama +RUN git checkout -q "$commit" && \ + GOOS=linux GOARCH=amd64 go build && \ + mkdir -p /go/build && \ + mv nakama /go/build + FROM alpine:3.7 MAINTAINER Heroic Labs @@ -12,12 +30,11 @@ RUN mkdir -p /nakama/data/modules && \ apk --no-cache add ca-certificates curl iproute2 WORKDIR /nakama/ -COPY ./nakama ./nakama +COPY --from=builder "/go/build/nakama" /nakama/ EXPOSE 7349 7350 7351 -# set entry point to nakama so that it can be invoked from the `docker run nakama` ENTRYPOINT ["./nakama"] -# curl fails on non-200 HTTP status code HEALTHCHECK --interval=5m --timeout=3s \ CMD curl -f http://localhost:7350/ || exit 1 + diff --git a/build/plugin.Dockerfile b/build/plugin.Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..a6e5e101474c987c4ab33ca118d03cfe11b86cb0 --- /dev/null +++ b/build/plugin.Dockerfile @@ -0,0 +1,14 @@ +# docker build . --file ./plugin.Dockerfile --build-arg src=sample_go_module + +FROM golang:1.10-alpine3.7 as builder + +ARG src + +WORKDIR /go/src/$src +COPY $src /go/src/$src + +RUN apk --no-cache add ca-certificates gcc musl-dev git && \ + go get -u github.com/heroiclabs/nakama && \ + GOOS=linux go build -buildmode=plugin . && \ + mkdir -p /go/build && \ + mv "/go/src/$src/$src.so" /go/build/ diff --git a/flags/flags.go b/flags/flags.go index 1bbd0d458b8e2ccfa5f090bc64410632e4cd1ba9..3f29be47a634b75328ca76cc63ca13a4f5c60425 100644 --- a/flags/flags.go +++ b/flags/flags.go @@ -352,9 +352,9 @@ func (fm *FlagMaker) enumerateAndCreate(prefix string, value reflect.Value, usag } usageDesc := fm.getUsage(optName, stField) - if len(usageDesc) == 0 { - optName = optName - } + //if len(usageDesc) == 0 { + // optName = optName + //} fm.enumerateAndCreate(optName, field, usageDesc) } @@ -450,7 +450,7 @@ func (fm *FlagMaker) defineFlag(name string, value reflect.Value, usage string) fm.fs.DurationVar(v, name, value.Interface().(time.Duration), usage) default: // (TODO) if one type defines time.Duration, we'll create a int64 flag for it. - // Find some acceptible way to deal with it. + // Find some acceptable way to deal with it. vv := ptrValue.Convert(int64PtrType).Interface().(*int64) fm.fs.Int64Var(vv, name, value.Int(), usage) } diff --git a/main.go b/main.go index e9df50e44a83a3474389bab7f1b4ad7594495059..a370f89861919d5093e065b186837cfc8cf19591 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,6 @@ import ( "os" "os/signal" "runtime" - "sync" "syscall" "time" @@ -95,40 +94,35 @@ func main() { // Access to social provider integrations. socialClient := social.NewClient(5 * time.Second) - // Used to govern once-per-server-start executions in all Lua runtime instances, across both pooled and match VMs. - once := &sync.Once{} // Start up server components. matchmaker := server.NewLocalMatchmaker(startupLogger, config.GetName()) sessionRegistry := server.NewSessionRegistry() tracker := server.StartLocalTracker(logger, sessionRegistry, jsonpbMarshaler, config.GetName()) router := server.NewLocalMessageRouter(sessionRegistry, tracker, jsonpbMarshaler) - stdLibs, modules, err := server.LoadRuntimeModules(startupLogger, config) - if err != nil { - startupLogger.Fatal("Failed reading runtime modules", zap.Error(err)) - } leaderboardCache := server.NewLocalLeaderboardCache(logger, startupLogger, db) - matchRegistry := server.NewLocalMatchRegistry(logger, db, config, socialClient, leaderboardCache, sessionRegistry, tracker, router, stdLibs, once, config.GetName()) + matchRegistry := server.NewLocalMatchRegistry(logger, config, tracker, config.GetName()) tracker.SetMatchJoinListener(matchRegistry.Join) tracker.SetMatchLeaveListener(matchRegistry.Leave) - // Separate module evaluation/validation from module loading. - // We need the match registry to be available to wire all functions exposed to the runtime, which in turn needs the modules at least cached first. - regCallbacks, err := server.ValidateRuntimeModules(logger, startupLogger, db, config, socialClient, leaderboardCache, sessionRegistry, matchRegistry, tracker, router, stdLibs, modules, once) + runtime, err := server.NewRuntime(logger, startupLogger, db, jsonpbMarshaler, jsonpbUnmarshaler, config, socialClient, leaderboardCache, sessionRegistry, matchRegistry, tracker, router) if err != nil { startupLogger.Fatal("Failed initializing runtime modules", zap.Error(err)) } - runtimePool := server.NewRuntimePool(logger, startupLogger, db, config, socialClient, leaderboardCache, sessionRegistry, matchRegistry, tracker, router, stdLibs, modules, regCallbacks, once) - pipeline := server.NewPipeline(config, db, jsonpbMarshaler, jsonpbUnmarshaler, sessionRegistry, matchRegistry, matchmaker, tracker, router, runtimePool) + pipeline := server.NewPipeline(logger, config, db, jsonpbMarshaler, jsonpbUnmarshaler, sessionRegistry, matchRegistry, matchmaker, tracker, router, runtime) metrics := server.NewMetrics(logger, startupLogger, config) consoleServer := server.StartConsoleServer(logger, startupLogger, config, db) - apiServer := server.StartApiServer(logger, startupLogger, db, jsonpbMarshaler, jsonpbUnmarshaler, config, socialClient, leaderboardCache, sessionRegistry, matchRegistry, matchmaker, tracker, router, pipeline, runtimePool) + apiServer := server.StartApiServer(logger, startupLogger, db, jsonpbMarshaler, jsonpbUnmarshaler, config, socialClient, leaderboardCache, sessionRegistry, matchRegistry, matchmaker, tracker, router, pipeline, runtime) gaenabled := len(os.Getenv("NAKAMA_TELEMETRY")) < 1 cookie := newOrLoadCookie(config) gacode := "UA-89792135-1" + var telemetryClient *http.Client if gaenabled { - runTelemetry(http.DefaultClient, gacode, cookie) + telemetryClient = &http.Client{ + Timeout: 1500 * time.Millisecond, + } + runTelemetry(telemetryClient, gacode, cookie) } // Respect OS stop signals. @@ -150,7 +144,7 @@ func main() { sessionRegistry.Stop() if gaenabled { - ga.SendSessionStop(http.DefaultClient, gacode, cookie) + ga.SendSessionStop(telemetryClient, gacode, cookie) } os.Exit(0) diff --git a/runtime/runtime.go b/runtime/runtime.go new file mode 100644 index 0000000000000000000000000000000000000000..efc98011886f16ba654c93435086c59cb6be9df2 --- /dev/null +++ b/runtime/runtime.go @@ -0,0 +1,300 @@ +// Copyright 2018 The Nakama Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtime + +import ( + "context" + "database/sql" + "github.com/golang/protobuf/ptypes/empty" + "github.com/heroiclabs/nakama/api" + "github.com/heroiclabs/nakama/rtapi" + "log" +) + +const ( + RUNTIME_CTX_ENV = "env" + RUNTIME_CTX_MODE = "execution_mode" + RUNTIME_CTX_QUERY_PARAMS = "query_params" + RUNTIME_CTX_USER_ID = "user_id" + RUNTIME_CTX_USERNAME = "username" + RUNTIME_CTX_USER_SESSION_EXP = "user_session_exp" + RUNTIME_CTX_SESSION_ID = "session_id" + RUNTIME_CTX_CLIENT_IP = "client_ip" + RUNTIME_CTX_CLIENT_PORT = "client_port" + RUNTIME_CTX_MATCH_ID = "match_id" + RUNTIME_CTX_MATCH_NODE = "match_node" + RUNTIME_CTX_MATCH_LABEL = "match_label" + RUNTIME_CTX_MATCH_TICK_RATE = "match_tick_rate" +) + +type Initializer interface { + RegisterRpc(id string, fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, payload string) (string, error, int)) error + + RegisterBeforeRt(id string, fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, envelope *rtapi.Envelope) (*rtapi.Envelope, error)) error + RegisterAfterRt(id string, fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, envelope *rtapi.Envelope) error) error + + RegisterBeforeGetAccount(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *empty.Empty) (*empty.Empty, error, int)) error + RegisterAfterGetAccount(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *api.Account) error) error + RegisterBeforeUpdateAccount(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.UpdateAccountRequest) (*api.UpdateAccountRequest, error, int)) error + RegisterAfterUpdateAccount(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeAuthenticateCustom(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.AuthenticateCustomRequest) (*api.AuthenticateCustomRequest, error, int)) error + RegisterAfterAuthenticateCustom(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *api.Session) error) error + RegisterBeforeAuthenticateDevice(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.AuthenticateDeviceRequest) (*api.AuthenticateDeviceRequest, error, int)) error + RegisterAfterAuthenticateDevice(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *api.Session) error) error + RegisterBeforeAuthenticateEmail(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.AuthenticateEmailRequest) (*api.AuthenticateEmailRequest, error, int)) error + RegisterAfterAuthenticateEmail(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *api.Session) error) error + RegisterBeforeAuthenticateFacebook(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.AuthenticateFacebookRequest) (*api.AuthenticateFacebookRequest, error, int)) error + RegisterAfterAuthenticateFacebook(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *api.Session) error) error + RegisterBeforeAuthenticateGameCenter(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.AuthenticateGameCenterRequest) (*api.AuthenticateGameCenterRequest, error, int)) error + RegisterAfterAuthenticateGameCenter(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *api.Session) error) error + RegisterBeforeAuthenticateGoogle(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.AuthenticateGoogleRequest) (*api.AuthenticateGoogleRequest, error, int)) error + RegisterAfterAuthenticateGoogle(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *api.Session) error) error + RegisterBeforeAuthenticateSteam(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.AuthenticateSteamRequest) (*api.AuthenticateSteamRequest, error, int)) error + RegisterAfterAuthenticateSteam(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *api.Session) error) error + RegisterBeforeListChannelMessages(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.ListChannelMessagesRequest) (*api.ListChannelMessagesRequest, error, int)) error + RegisterAfterListChannelMessages(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *api.ChannelMessageList) error) error + RegisterBeforeListFriends(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *empty.Empty) (*empty.Empty, error, int)) error + RegisterAfterListFriends(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *api.Friends) error) error + RegisterBeforeAddFriends(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.AddFriendsRequest) (*api.AddFriendsRequest, error, int)) error + RegisterAfterAddFriends(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeDeleteFriends(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.DeleteFriendsRequest) (*api.DeleteFriendsRequest, error, int)) error + RegisterAfterDeleteFriends(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeBlockFriends(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.BlockFriendsRequest) (*api.BlockFriendsRequest, error, int)) error + RegisterAfterBlockFriends(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeImportFacebookFriends(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.ImportFacebookFriendsRequest) (*api.ImportFacebookFriendsRequest, error, int)) error + RegisterAfterImportFacebookFriends(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeCreateGroup(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.CreateGroupRequest) (*api.CreateGroupRequest, error, int)) error + RegisterAfterCreateGroup(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *api.Group) error) error + RegisterBeforeUpdateGroup(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.UpdateGroupRequest) (*api.UpdateGroupRequest, error, int)) error + RegisterAfterUpdateGroup(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeDeleteGroup(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.DeleteGroupRequest) (*api.DeleteGroupRequest, error, int)) error + RegisterAfterDeleteGroup(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeJoinGroup(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.JoinGroupRequest) (*api.JoinGroupRequest, error, int)) error + RegisterAfterJoinGroup(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeLeaveGroup(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.LeaveGroupRequest) (*api.LeaveGroupRequest, error, int)) error + RegisterAfterLeaveGroup(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeAddGroupUsers(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.AddGroupUsersRequest) (*api.AddGroupUsersRequest, error, int)) error + RegisterAfterAddGroupUsers(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeKickGroupUsers(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.KickGroupUsersRequest) (*api.KickGroupUsersRequest, error, int)) error + RegisterAfterKickGroupUsers(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforePromoteGroupUsers(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.PromoteGroupUsersRequest) (*api.PromoteGroupUsersRequest, error, int)) error + RegisterAfterPromoteGroupUsers(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeListGroupUsers(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.ListGroupUsersRequest) (*api.ListGroupUsersRequest, error, int)) error + RegisterAfterListGroupUsers(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *api.GroupUserList) error) error + RegisterBeforeListUserGroups(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.ListUserGroupsRequest) (*api.ListUserGroupsRequest, error, int)) error + RegisterAfterListUserGroups(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *api.UserGroupList) error) error + RegisterBeforeListGroups(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.ListGroupsRequest) (*api.ListGroupsRequest, error, int)) error + RegisterAfterListGroups(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *api.GroupList) error) error + RegisterBeforeDeleteLeaderboardRecord(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.DeleteLeaderboardRecordRequest) (*api.DeleteLeaderboardRecordRequest, error, int)) error + RegisterAfterDeleteLeaderboardRecord(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeListLeaderboardRecords(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.ListLeaderboardRecordsRequest) (*api.ListLeaderboardRecordsRequest, error, int)) error + RegisterAfterListLeaderboardRecords(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *api.LeaderboardRecordList) error) error + RegisterBeforeWriteLeaderboardRecord(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.WriteLeaderboardRecordRequest) (*api.WriteLeaderboardRecordRequest, error, int)) error + RegisterAfterWriteLeaderboardRecord(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *api.LeaderboardRecord) error) error + RegisterBeforeLinkCustom(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.AccountCustom) (*api.AccountCustom, error, int)) error + RegisterAfterLinkCustom(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeLinkDevice(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.AccountDevice) (*api.AccountDevice, error, int)) error + RegisterAfterLinkDevice(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeLinkEmail(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.AccountEmail) (*api.AccountEmail, error, int)) error + RegisterAfterLinkEmail(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeLinkFacebook(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.LinkFacebookRequest) (*api.LinkFacebookRequest, error, int)) error + RegisterAfterLinkFacebook(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeLinkGameCenter(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.AccountGameCenter) (*api.AccountGameCenter, error, int)) error + RegisterAfterLinkGameCenter(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeLinkGoogle(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.AccountGoogle) (*api.AccountGoogle, error, int)) error + RegisterAfterLinkGoogle(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeLinkSteam(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.AccountSteam) (*api.AccountSteam, error, int)) error + RegisterAfterLinkSteam(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeListMatches(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.ListMatchesRequest) (*api.ListMatchesRequest, error, int)) error + RegisterAfterListMatches(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *api.MatchList) error) error + RegisterBeforeListNotifications(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.ListNotificationsRequest) (*api.ListNotificationsRequest, error, int)) error + RegisterAfterListNotifications(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *api.NotificationList) error) error + RegisterBeforeDeleteNotification(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.DeleteNotificationsRequest) (*api.DeleteNotificationsRequest, error, int)) error + RegisterAfterDeleteNotification(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeListStorageObjects(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.ListStorageObjectsRequest) (*api.ListStorageObjectsRequest, error, int)) error + RegisterAfterListStorageObjects(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *api.StorageObjectList) error) error + RegisterBeforeReadStorageObjects(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.ReadStorageObjectsRequest) (*api.ReadStorageObjectsRequest, error, int)) error + RegisterAfterReadStorageObjects(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *api.StorageObjects) error) error + RegisterBeforeWriteStorageObjects(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.WriteStorageObjectsRequest) (*api.WriteStorageObjectsRequest, error, int)) error + RegisterAfterWriteStorageObjects(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *api.StorageObjectAcks) error) error + RegisterBeforeDeleteStorageObjects(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.DeleteStorageObjectsRequest) (*api.DeleteStorageObjectsRequest, error, int)) error + RegisterAfterDeleteStorageObjects(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeUnlinkCustom(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.AccountCustom) (*api.AccountCustom, error, int)) error + RegisterAfterUnlinkCustom(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeUnlinkDevice(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.AccountDevice) (*api.AccountDevice, error, int)) error + RegisterAfterUnlinkDevice(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeUnlinkEmail(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.AccountEmail) (*api.AccountEmail, error, int)) error + RegisterAfterUnlinkEmail(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeUnlinkFacebook(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.AccountFacebook) (*api.AccountFacebook, error, int)) error + RegisterAfterUnlinkFacebook(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeUnlinkGameCenter(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.AccountGameCenter) (*api.AccountGameCenter, error, int)) error + RegisterAfterUnlinkGameCenter(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeUnlinkGoogle(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.AccountGoogle) (*api.AccountGoogle, error, int)) error + RegisterAfterUnlinkGoogle(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeUnlinkSteam(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.AccountSteam) (*api.AccountSteam, error, int)) error + RegisterAfterUnlinkSteam(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *empty.Empty) error) error + RegisterBeforeGetUsers(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, in *api.GetUsersRequest) (*api.GetUsersRequest, error, int)) error + RegisterAfterGetUsers(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, out *api.Users) error) error + + RegisterMatchmakerMatched(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, entries []MatchmakerEntry) (string, error)) error + + RegisterMatch(name string, fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule) (Match, error)) error +} + +type PresenceMeta interface { + GetHidden() bool + GetPersistence() bool + GetUsername() string + GetStatus() string +} + +type Presence interface { + PresenceMeta + GetUserId() string + GetSessionId() string + GetNodeId() string +} + +type MatchmakerEntry interface { + GetPresence() Presence + GetTicket() string + GetProperties() map[string]interface{} +} + +type MatchData interface { + Presence + GetOpCode() int64 + GetData() []byte + GetReceiveTime() int64 +} + +type MatchDispatcher interface { + BroadcastMessage(opCode int64, data []byte, presences []Presence, sender Presence) error + MatchKick(presences []Presence) error + MatchLabelUpdate(label string) error +} + +type Match interface { + MatchInit(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, params map[string]interface{}) (interface{}, int, string) + MatchJoinAttempt(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, dispatcher MatchDispatcher, tick int64, state interface{}, presence Presence) (interface{}, bool, string) + MatchJoin(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, dispatcher MatchDispatcher, tick int64, state interface{}, presences []Presence) interface{} + MatchLeave(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, dispatcher MatchDispatcher, tick int64, state interface{}, presences []Presence) interface{} + MatchLoop(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, dispatcher MatchDispatcher, tick int64, state interface{}, messages []MatchData) interface{} +} + +type NotificationSend struct { + UserID string + Subject string + Content map[string]interface{} + Code int + Sender string + Persistent bool +} + +type WalletUpdate struct { + UserID string + Changeset map[string]interface{} + Metadata map[string]interface{} +} + +type WalletLedgerItem interface { + GetID() string + GetUserID() string + GetCreateTime() int64 + GetUpdateTime() int64 + GetChangeset() map[string]interface{} + GetMetadata() map[string]interface{} +} + +type StorageRead struct { + Collection string + Key string + UserID string +} + +type StorageWrite struct { + Collection string + Key string + UserID string + Value string + Version string + PermissionRead int + PermissionWrite int +} + +type StorageDelete struct { + Collection string + Key string + UserID string + Version string +} + +type NakamaModule interface { + AuthenticateCustom(id, username string, create bool) (string, string, bool, error) + AuthenticateDevice(id, username string, create bool) (string, string, bool, error) + AuthenticateEmail(email, password, username string, create bool) (string, string, bool, error) + AuthenticateFacebook(token string, importFriends bool, username string, create bool) (string, string, bool, error) + AuthenticateGameCenter(playerID, bundleID string, timestamp int64, salt, signature, publicKeyUrl, username string, create bool) (string, string, bool, error) + AuthenticateGoogle(token, username string, create bool) (string, string, bool, error) + AuthenticateSteam(token, username string, create bool) (string, string, bool, error) + + AuthenticateTokenGenerate(userID, username string, exp int64) (string, int64, error) + + AccountGetId(userID string) (*api.Account, error) + // TODO nullable fields? + AccountUpdateId(userID, username string, metadata map[string]interface{}, displayName, timezone, location, langTag, avatarUrl string) error + + UsersGetId(userIDs []string) ([]*api.User, error) + UsersGetUsername(usernames []string) ([]*api.User, error) + UsersBanId(userIDs []string) error + UsersUnbanId(userIDs []string) error + + StreamUserList(mode uint8, subject, descriptor, label string, includeHidden, includeNotHidden bool) ([]Presence, error) + StreamUserGet(mode uint8, subject, descriptor, label, userID, sessionID string) (PresenceMeta, error) + StreamUserJoin(mode uint8, subject, descriptor, label, userID, sessionID string, hidden, persistence bool, status string) (bool, error) + StreamUserUpdate(mode uint8, subject, descriptor, label, userID, sessionID string, hidden, persistence bool, status string) error + StreamUserLeave(mode uint8, subject, descriptor, label, userID, sessionID string) error + StreamCount(mode uint8, subject, descriptor, label string) (int, error) + StreamClose(mode uint8, subject, descriptor, label string) error + StreamSend(mode uint8, subject, descriptor, label, data string) error + + MatchCreate(module string, params map[string]interface{}) (string, error) + MatchList(limit int, authoritative bool, label string, minSize, maxSize int) []*api.Match + + NotificationSend(userID, subject string, content map[string]interface{}, code int, sender string, persistent bool) error + NotificationsSend(notifications []*NotificationSend) error + + WalletUpdate(userID string, changeset, metadata map[string]interface{}) error + WalletsUpdate(updates []*WalletUpdate) error + WalletLedgerUpdate(itemID string, metadata map[string]interface{}) (WalletLedgerItem, error) + WalletLedgerList(userID string) ([]WalletLedgerItem, error) + + StorageList(userID, collection string, limit int, cursor string) ([]*api.StorageObject, string, error) + StorageRead(reads []*StorageRead) ([]*api.StorageObject, error) + StorageWrite(writes []*StorageWrite) ([]*api.StorageObjectAck, error) + StorageDelete(deletes []*StorageDelete) error + + LeaderboardCreate(id string, authoritative bool, sortOrder, operator, resetSchedule string, metadata map[string]interface{}) error + LeaderboardDelete(id string) error + LeaderboardRecordsList(id string, ownerIDs []string, limit int, cursor string) ([]*api.LeaderboardRecord, []*api.LeaderboardRecord, string, string, error) + LeaderboardRecordWrite(id, ownerID, username string, score, subscore int64, metadata map[string]interface{}) (*api.LeaderboardRecord, error) + LeaderboardRecordDelete(id, ownerID string) error + + GroupCreate(userID, name, creatorID, langTag, description, avatarUrl string, open bool, metadata map[string]interface{}, maxCount int) (*api.Group, error) + GroupUpdate(id, name, creatorID, langTag, description, avatarUrl string, open bool, metadata map[string]interface{}, maxCount int) error + GroupDelete(id string) error + GroupUsersList(id string) ([]*api.GroupUserList_GroupUser, error) + UserGroupsList(userID string) ([]*api.UserGroupList_UserGroup, error) +} diff --git a/sample_go_module/README.md b/sample_go_module/README.md new file mode 100644 index 0000000000000000000000000000000000000000..4371d4e4f2b4f783cd73592bbd2104d18707697e --- /dev/null +++ b/sample_go_module/README.md @@ -0,0 +1,5 @@ +``` +go build -buildmode=plugin + +cp sample_go_module.so ../data/modules/sample_go_module.so +``` diff --git a/sample_go_module/sample.go b/sample_go_module/sample.go new file mode 100644 index 0000000000000000000000000000000000000000..d65822fcf53201938bf76c706fab2d947ab30d46 --- /dev/null +++ b/sample_go_module/sample.go @@ -0,0 +1,107 @@ +// Copyright 2018 The Nakama Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "database/sql" + "github.com/heroiclabs/nakama/rtapi" + "github.com/heroiclabs/nakama/runtime" + "log" +) + +func InitModule(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) { + initializer.RegisterRpc("go_echo_sample", rpcEcho) + initializer.RegisterBeforeRt("ChannelJoin", beforeChannelJoin) + initializer.RegisterMatch("match", func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule) (runtime.Match, error) { + return &Match{}, nil + }) +} + +func rpcEcho(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error, int) { + logger.Print("RUNNING IN GO") + return payload, nil, 0 +} + +func beforeChannelJoin(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, envelope *rtapi.Envelope) (*rtapi.Envelope, error) { + logger.Printf("Intercepted request to join channel '%v'", envelope.GetChannelJoin().Target) + return envelope, nil +} + +type MatchState struct { + debug bool +} + +type Match struct{} + +func (m *Match) MatchInit(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, params map[string]interface{}) (interface{}, int, string) { + var debug bool + if d, ok := params["debug"]; ok { + if dv, ok := d.(bool); ok { + debug = dv + } + } + state := &MatchState{ + debug: debug, + } + + if state.debug { + logger.Printf("match init, starting with debug: %v", state.debug) + } + tickRate := 1 + label := "skill=100-150" + + return state, tickRate, label +} + +func (m *Match) MatchJoinAttempt(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, presence runtime.Presence) (interface{}, bool, string) { + if state.(*MatchState).debug { + logger.Printf("match join attempt username %v user_id %v session_id %v node %v", presence.GetUsername(), presence.GetUserId(), presence.GetSessionId(), presence.GetNodeId()) + } + + return state, true, "" +} + +func (m *Match) MatchJoin(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, presences []runtime.Presence) interface{} { + if state.(*MatchState).debug { + for _, presence := range presences { + logger.Printf("match join username %v user_id %v session_id %v node %v", presence.GetUsername(), presence.GetUserId(), presence.GetSessionId(), presence.GetNodeId()) + } + } + + return state +} + +func (m *Match) MatchLeave(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, presences []runtime.Presence) interface{} { + if state.(*MatchState).debug { + for _, presence := range presences { + logger.Printf("match leave username %v user_id %v session_id %v node %v", presence.GetUsername(), presence.GetUserId(), presence.GetSessionId(), presence.GetNodeId()) + } + } + + return state +} + +func (m *Match) MatchLoop(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, messages []runtime.MatchData) interface{} { + if state.(*MatchState).debug { + logger.Printf("match loop match_id %v tick %v", ctx.Value(runtime.RUNTIME_CTX_MATCH_ID), tick) + logger.Printf("match loop match_id %v message count %v", ctx.Value(runtime.RUNTIME_CTX_MATCH_ID), len(messages)) + } + + if tick >= 10 { + return nil + } + return state +} diff --git a/server/api.go b/server/api.go index 9d0ef6d731f0e6ce015f13a2423b51a620359a13..c10fb0f9cd18af55369f251053c7bf39b8f7f9b1 100644 --- a/server/api.go +++ b/server/api.go @@ -19,6 +19,7 @@ import ( "database/sql" "encoding/base64" "fmt" + "google.golang.org/grpc/peer" "net" "net/http" "strings" @@ -35,14 +36,11 @@ import ( "github.com/golang/protobuf/ptypes/empty" "github.com/gorilla/handlers" "github.com/gorilla/mux" - "github.com/grpc-ecosystem/grpc-gateway/runtime" + grpcRuntime "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/heroiclabs/nakama/api" "github.com/heroiclabs/nakama/social" "go.opencensus.io/plugin/ocgrpc" "go.opencensus.io/plugin/ochttp" - "go.opencensus.io/stats" - "go.opencensus.io/tag" - "go.opencensus.io/trace" "go.uber.org/zap" "golang.org/x/net/context" "google.golang.org/grpc" @@ -50,7 +48,6 @@ import ( "google.golang.org/grpc/credentials" _ "google.golang.org/grpc/encoding/gzip" // enable gzip compression on server for grpc "google.golang.org/grpc/metadata" - "google.golang.org/grpc/peer" "google.golang.org/grpc/status" ) @@ -59,6 +56,8 @@ type ctxUserIDKey struct{} type ctxUsernameKey struct{} type ctxExpiryKey struct{} +type ctxFullMethodKey struct{} + type ApiServer struct { logger *zap.Logger db *sql.DB @@ -68,16 +67,22 @@ type ApiServer struct { matchRegistry MatchRegistry tracker Tracker router MessageRouter - runtimePool *RuntimePool + runtime *Runtime grpcServer *grpc.Server grpcGatewayServer *http.Server } -func StartApiServer(logger *zap.Logger, startupLogger *zap.Logger, db *sql.DB, jsonpbMarshaler *jsonpb.Marshaler, jsonpbUnmarshaler *jsonpb.Unmarshaler, config Config, socialClient *social.Client, leaderboardCache LeaderboardCache, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, matchmaker Matchmaker, tracker Tracker, router MessageRouter, pipeline *Pipeline, runtimePool *RuntimePool) *ApiServer { +func StartApiServer(logger *zap.Logger, startupLogger *zap.Logger, db *sql.DB, jsonpbMarshaler *jsonpb.Marshaler, jsonpbUnmarshaler *jsonpb.Unmarshaler, config Config, socialClient *social.Client, leaderboardCache LeaderboardCache, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, matchmaker Matchmaker, tracker Tracker, router MessageRouter, pipeline *Pipeline, runtime *Runtime) *ApiServer { serverOpts := []grpc.ServerOption{ grpc.StatsHandler(&ocgrpc.ServerHandler{IsPublicEndpoint: true}), grpc.MaxRecvMsgSize(int(config.GetSocket().MaxMessageSizeBytes)), - grpc.UnaryInterceptor(apiInterceptorFunc(logger, config, runtimePool, jsonpbMarshaler, jsonpbUnmarshaler)), + grpc.UnaryInterceptor(func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + ctx, err := securityInterceptorFunc(logger, config, ctx, req, info) + if err != nil { + return nil, err + } + return handler(ctx, req) + }), } if config.GetSocket().TLSCert != nil { serverOpts = append(serverOpts, grpc.Creds(credentials.NewServerTLSFromCert(&config.GetSocket().TLSCert[0]))) @@ -93,7 +98,7 @@ func StartApiServer(logger *zap.Logger, startupLogger *zap.Logger, db *sql.DB, j matchRegistry: matchRegistry, tracker: tracker, router: router, - runtimePool: runtimePool, + runtime: runtime, grpcServer: grpcServer, } @@ -114,8 +119,8 @@ func StartApiServer(logger *zap.Logger, startupLogger *zap.Logger, db *sql.DB, j // Register and start GRPC Gateway server. // Should start after GRPC server itself because RegisterNakamaHandlerFromEndpoint below tries to dial GRPC. ctx := context.Background() - grpcGateway := runtime.NewServeMux( - runtime.WithMetadata(func(ctx context.Context, r *http.Request) metadata.MD { + grpcGateway := grpcRuntime.NewServeMux( + grpcRuntime.WithMetadata(func(ctx context.Context, r *http.Request) metadata.MD { // For RPC GET operations pass through any custom query parameters. if r.Method != "GET" || !strings.HasPrefix(r.URL.Path, "/v2/rpc/") { return metadata.MD{} @@ -221,106 +226,11 @@ func (s *ApiServer) Healthcheck(ctx context.Context, in *empty.Empty) (*empty.Em return &empty.Empty{}, nil } -func apiInterceptorFunc(logger *zap.Logger, config Config, runtimePool *RuntimePool, jsonpbMarshaler *jsonpb.Marshaler, jsonpbUnmarshaler *jsonpb.Unmarshaler) func(context.Context, interface{}, *grpc.UnaryServerInfo, grpc.UnaryHandler) (interface{}, error) { - return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - //logger.Debug("Security interceptor fired", zap.Any("ctx", ctx), zap.Any("req", req), zap.Any("info", info)) - ctx, err := securityInterceptorFunc(logger, config, ctx, req, info) - if err != nil { - return nil, err - } - - switch info.FullMethod { - case "/nakama.api.Nakama/Healthcheck": - fallthrough - case "/nakama.api.Nakama/RpcFunc": - return handler(ctx, req) - } - - uid := uuid.Nil - username := "" - expiry := int64(0) - if ctx.Value(ctxUserIDKey{}) != nil { - // incase of authentication methods, uid is nil - uid = ctx.Value(ctxUserIDKey{}).(uuid.UUID) - username = ctx.Value(ctxUsernameKey{}).(string) - expiry = ctx.Value(ctxExpiryKey{}).(int64) - } - - // Method name to use for before/after stats. - var methodName string - if parts := strings.SplitN(info.FullMethod, "/", 3); len(parts) == 3 { - methodName = parts[2] - } - - // Stats measurement start boundary. - name := fmt.Sprintf("nakama.api-before.Nakama.%v", methodName) - statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) - startNanos := time.Now().UTC().UnixNano() - span := trace.NewSpan(name, nil, trace.StartOptions{}) - - clientAddr := "" - clientIP := "" - clientPort := "" - md, _ := metadata.FromIncomingContext(ctx) - if ips := md.Get("x-forwarded-for"); len(ips) > 0 { - // look for gRPC-Gateway / LB header - clientAddr = strings.Split(ips[0], ",")[0] - } else if peerInfo, ok := peer.FromContext(ctx); ok { - // if missing, try to look up gRPC peer info - clientAddr = peerInfo.Addr.String() - } - - clientAddr = strings.TrimSpace(clientAddr) - if host, port, err := net.SplitHostPort(clientAddr); err == nil { - clientIP = host - clientPort = port - } else if addrErr, ok := err.(*net.AddrError); ok && addrErr.Err == "missing port in address" { - clientIP = clientAddr - } else { - logger.Debug("Could not extract client address from request.", zap.Error(err)) - } - - // Actual before hook function execution. - beforeHookResult, hookErr := invokeReqBeforeHook(logger, config, runtimePool, jsonpbMarshaler, jsonpbUnmarshaler, "", uid, username, expiry, clientIP, clientPort, info.FullMethod, req) - - // Stats measurement end boundary. - span.End() - stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) - - if hookErr != nil { - return nil, hookErr - } else if beforeHookResult == nil { - // if result is nil, requested resource is disabled. - logger.Warn("Intercepted a disabled resource.", - zap.String("resource", info.FullMethod), - zap.String("uid", uid.String()), - zap.String("username", username)) - return nil, status.Error(codes.NotFound, "Requested resource was not found.") - } - - handlerResult, handlerErr := handler(ctx, beforeHookResult) - if handlerErr == nil { - // Stats measurement start boundary. - name := fmt.Sprintf("nakama.api-after.Nakama.%v", methodName) - statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) - startNanos := time.Now().UTC().UnixNano() - span := trace.NewSpan(name, nil, trace.StartOptions{}) - // Actual after hook function execution. - invokeReqAfterHook(logger, config, runtimePool, jsonpbMarshaler, "", uid, username, expiry, clientIP, clientPort, info.FullMethod, handlerResult) - - // Stats measurement end boundary. - span.End() - stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) - } - return handlerResult, handlerErr - } -} - func securityInterceptorFunc(logger *zap.Logger, config Config, ctx context.Context, req interface{}, info *grpc.UnaryServerInfo) (context.Context, error) { switch info.FullMethod { case "/nakama.api.Nakama/Healthcheck": // Healthcheck has no security. - return nil, nil + return ctx, nil case "/nakama.api.Nakama/AuthenticateCustom": fallthrough case "/nakama.api.Nakama/AuthenticateDevice": @@ -425,7 +335,7 @@ func securityInterceptorFunc(logger *zap.Logger, config Config, ctx context.Cont } ctx = context.WithValue(context.WithValue(context.WithValue(ctx, ctxUserIDKey{}, userID), ctxUsernameKey{}, username), ctxExpiryKey{}, exp) } - return ctx, nil + return context.WithValue(ctx, ctxFullMethodKey{}, info.FullMethod), nil } func parseBasicAuth(auth string) (username, password string, ok bool) { @@ -498,3 +408,29 @@ func decompressHandler(logger *zap.Logger, h http.Handler) http.HandlerFunc { h.ServeHTTP(w, r) }) } + +func extractClientAddress(logger *zap.Logger, ctx context.Context) (string, string) { + clientAddr := "" + clientIP := "" + clientPort := "" + md, _ := metadata.FromIncomingContext(ctx) + if ips := md.Get("x-forwarded-for"); len(ips) > 0 { + // Look for gRPC-Gateway / LB header. + clientAddr = strings.Split(ips[0], ",")[0] + } else if peerInfo, ok := peer.FromContext(ctx); ok { + // If missing, try to look up gRPC peer info. + clientAddr = peerInfo.Addr.String() + } + + clientAddr = strings.TrimSpace(clientAddr) + if host, port, err := net.SplitHostPort(clientAddr); err == nil { + clientIP = host + clientPort = port + } else if addrErr, ok := err.(*net.AddrError); ok && addrErr.Err == "missing port in address" { + clientIP = clientAddr + } else { + logger.Debug("Could not extract client address from request.", zap.Error(err)) + } + + return clientIP, clientPort +} diff --git a/server/api_account.go b/server/api_account.go index 4d57664a5303bd107c8a2204414f8cf4680381b2..71ad4dde381956331f8124187233b659bbff2d53 100644 --- a/server/api_account.go +++ b/server/api_account.go @@ -15,18 +15,51 @@ package server import ( + "fmt" "github.com/gofrs/uuid" "github.com/golang/protobuf/ptypes/empty" "github.com/heroiclabs/nakama/api" "github.com/lib/pq" + "go.opencensus.io/stats" + "go.opencensus.io/tag" + "go.opencensus.io/trace" + "go.uber.org/zap" "golang.org/x/net/context" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "time" ) func (s *ApiServer) GetAccount(ctx context.Context, in *empty.Empty) (*api.Account, error) { userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeGetAccountFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + user, err := GetAccount(s.logger, s.db, s.tracker, userID) if err != nil { if err == ErrAccountNotFound { @@ -35,10 +68,56 @@ func (s *ApiServer) GetAccount(ctx context.Context, in *empty.Empty) (*api.Accou return nil, status.Error(codes.Internal, "Error retrieving user account.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterGetAccountFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, user) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return user, nil } func (s *ApiServer) UpdateAccount(ctx context.Context, in *api.UpdateAccountRequest) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeUpdateAccountFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + username := in.GetUsername().GetValue() if in.GetUsername() != nil { if len(username) < 1 || len(username) > 128 { @@ -46,16 +125,30 @@ func (s *ApiServer) UpdateAccount(ctx context.Context, in *api.UpdateAccountRequ } } - userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) err := UpdateAccount(s.db, s.logger, userID, username, in.GetDisplayName(), in.GetTimezone(), in.GetLocation(), in.GetLangTag(), in.GetAvatarUrl(), nil) - if err != nil { if _, ok := err.(*pq.Error); ok { return nil, status.Error(codes.Internal, "Error while trying to update account.") } - return nil, status.Error(codes.InvalidArgument, err.Error()) } + // After hook. + if fn := s.runtime.afterReqFunctions.afterUpdateAccountFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } diff --git a/server/api_authenticate.go b/server/api_authenticate.go index 048f07c24652326f06757a5d0976ecb29d5fea06..4822dd8cac64d0488c69c64c84d3769ea2227dd0 100644 --- a/server/api_authenticate.go +++ b/server/api_authenticate.go @@ -15,6 +15,11 @@ package server import ( + "fmt" + "go.opencensus.io/stats" + "go.opencensus.io/tag" + "go.opencensus.io/trace" + "go.uber.org/zap" "math/rand" "regexp" "strings" @@ -34,6 +39,33 @@ var ( ) func (s *ApiServer) AuthenticateCustom(ctx context.Context, in *api.AuthenticateCustomRequest) (*api.Session, error) { + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeAuthenticateCustomFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, "", "", 0, clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod)) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.Account == nil || in.Account.Id == "" { return nil, status.Error(codes.InvalidArgument, "Custom ID is required.") } else if invalidCharsRegex.MatchString(in.Account.Id) { @@ -58,11 +90,57 @@ func (s *ApiServer) AuthenticateCustom(ctx context.Context, in *api.Authenticate return nil, err } - token := generateToken(s.config, dbUserID, dbUsername) - return &api.Session{Created: created, Token: token}, nil + token, exp := generateToken(s.config, dbUserID, dbUsername) + session := &api.Session{Created: created, Token: token} + + // After hook. + if fn := s.runtime.afterReqFunctions.afterAuthenticateCustomFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, dbUserID, dbUsername, exp, clientIP, clientPort, session) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + + return session, nil } func (s *ApiServer) AuthenticateDevice(ctx context.Context, in *api.AuthenticateDeviceRequest) (*api.Session, error) { + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeAuthenticateDeviceFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, "", "", 0, clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod)) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.Account == nil || in.Account.Id == "" { return nil, status.Error(codes.InvalidArgument, "Device ID is required.") } else if invalidCharsRegex.MatchString(in.Account.Id) { @@ -87,11 +165,57 @@ func (s *ApiServer) AuthenticateDevice(ctx context.Context, in *api.Authenticate return nil, err } - token := generateToken(s.config, dbUserID, dbUsername) - return &api.Session{Created: created, Token: token}, nil + token, exp := generateToken(s.config, dbUserID, dbUsername) + session := &api.Session{Created: created, Token: token} + + // After hook. + if fn := s.runtime.afterReqFunctions.afterAuthenticateDeviceFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, dbUserID, dbUsername, exp, clientIP, clientPort, session) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + + return session, nil } func (s *ApiServer) AuthenticateEmail(ctx context.Context, in *api.AuthenticateEmailRequest) (*api.Session, error) { + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeAuthenticateEmailFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, "", "", 0, clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod)) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + email := in.Account if email == nil || email.Email == "" || email.Password == "" { return nil, status.Error(codes.InvalidArgument, "Email address and password is required.") @@ -123,11 +247,57 @@ func (s *ApiServer) AuthenticateEmail(ctx context.Context, in *api.AuthenticateE return nil, err } - token := generateToken(s.config, dbUserID, dbUsername) - return &api.Session{Created: created, Token: token}, nil + token, exp := generateToken(s.config, dbUserID, dbUsername) + session := &api.Session{Created: created, Token: token} + + // After hook. + if fn := s.runtime.afterReqFunctions.afterAuthenticateEmailFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, dbUserID, dbUsername, exp, clientIP, clientPort, session) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + + return session, nil } func (s *ApiServer) AuthenticateFacebook(ctx context.Context, in *api.AuthenticateFacebookRequest) (*api.Session, error) { + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeAuthenticateFacebookFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, "", "", 0, clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod)) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.Account == nil || in.Account.Token == "" { return nil, status.Error(codes.InvalidArgument, "Facebook access token is required.") } @@ -153,11 +323,57 @@ func (s *ApiServer) AuthenticateFacebook(ctx context.Context, in *api.Authentica importFacebookFriends(s.logger, s.db, s.router, s.socialClient, uuid.FromStringOrNil(dbUserID), dbUsername, in.Account.Token, false) } - token := generateToken(s.config, dbUserID, dbUsername) - return &api.Session{Created: created, Token: token}, nil + token, exp := generateToken(s.config, dbUserID, dbUsername) + session := &api.Session{Created: created, Token: token} + + // After hook. + if fn := s.runtime.afterReqFunctions.afterAuthenticateFacebookFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, dbUserID, dbUsername, exp, clientIP, clientPort, session) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + + return session, nil } func (s *ApiServer) AuthenticateGameCenter(ctx context.Context, in *api.AuthenticateGameCenterRequest) (*api.Session, error) { + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeAuthenticateGameCenterFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, "", "", 0, clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod)) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.Account == nil { return nil, status.Error(codes.InvalidArgument, "GameCenter access credentials are required.") } else if in.Account.BundleId == "" { @@ -190,11 +406,57 @@ func (s *ApiServer) AuthenticateGameCenter(ctx context.Context, in *api.Authenti return nil, err } - token := generateToken(s.config, dbUserID, dbUsername) - return &api.Session{Created: created, Token: token}, nil + token, exp := generateToken(s.config, dbUserID, dbUsername) + session := &api.Session{Created: created, Token: token} + + // After hook. + if fn := s.runtime.afterReqFunctions.afterAuthenticateGameCenterFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, dbUserID, dbUsername, exp, clientIP, clientPort, session) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + + return session, nil } func (s *ApiServer) AuthenticateGoogle(ctx context.Context, in *api.AuthenticateGoogleRequest) (*api.Session, error) { + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeAuthenticateGoogleFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, "", "", 0, clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod)) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.Account == nil || in.Account.Token == "" { return nil, status.Error(codes.InvalidArgument, "Google access token is required.") } @@ -215,11 +477,57 @@ func (s *ApiServer) AuthenticateGoogle(ctx context.Context, in *api.Authenticate return nil, err } - token := generateToken(s.config, dbUserID, dbUsername) - return &api.Session{Created: created, Token: token}, nil + token, exp := generateToken(s.config, dbUserID, dbUsername) + session := &api.Session{Created: created, Token: token} + + // After hook. + if fn := s.runtime.afterReqFunctions.afterAuthenticateGoogleFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, dbUserID, dbUsername, exp, clientIP, clientPort, session) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + + return session, nil } func (s *ApiServer) AuthenticateSteam(ctx context.Context, in *api.AuthenticateSteamRequest) (*api.Session, error) { + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeAuthenticateSteamFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, "", "", 0, clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod)) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if s.config.GetSocial().Steam.PublisherKey == "" || s.config.GetSocial().Steam.AppID == 0 { return nil, status.Error(codes.FailedPrecondition, "Steam authentication is not configured.") } @@ -244,23 +552,42 @@ func (s *ApiServer) AuthenticateSteam(ctx context.Context, in *api.AuthenticateS return nil, err } - token := generateToken(s.config, dbUserID, dbUsername) - return &api.Session{Created: created, Token: token}, nil + token, exp := generateToken(s.config, dbUserID, dbUsername) + session := &api.Session{Created: created, Token: token} + + // After hook. + if fn := s.runtime.afterReqFunctions.afterAuthenticateSteamFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, dbUserID, dbUsername, exp, clientIP, clientPort, session) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + + return session, nil } -func generateToken(config Config, userID, username string) string { +func generateToken(config Config, userID, username string) (string, int64) { exp := time.Now().UTC().Add(time.Duration(config.GetSession().TokenExpirySec) * time.Second).Unix() return generateTokenWithExpiry(config, userID, username, exp) } -func generateTokenWithExpiry(config Config, userID, username string, exp int64) string { +func generateTokenWithExpiry(config Config, userID, username string, exp int64) (string, int64) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "uid": userID, "exp": exp, "usn": username, }) signedToken, _ := token.SignedString([]byte(config.GetSession().EncryptionKey)) - return signedToken + return signedToken, exp } func generateUsername() string { diff --git a/server/api_channel.go b/server/api_channel.go index c1703b5e86642122bf5c59a1a20d769c8c2b07f4..332c9c0f0a5dc409b51171324db71468634366fc 100644 --- a/server/api_channel.go +++ b/server/api_channel.go @@ -15,14 +15,49 @@ package server import ( + "fmt" "github.com/gofrs/uuid" "github.com/heroiclabs/nakama/api" + "go.opencensus.io/stats" + "go.opencensus.io/tag" + "go.opencensus.io/trace" + "go.uber.org/zap" "golang.org/x/net/context" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "time" ) func (s *ApiServer) ListChannelMessages(ctx context.Context, in *api.ListChannelMessagesRequest) (*api.ChannelMessageList, error) { + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeListChannelMessagesFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.ChannelId == "" { return nil, status.Error(codes.InvalidArgument, "Invalid channel ID.") } @@ -45,7 +80,6 @@ func (s *ApiServer) ListChannelMessages(ctx context.Context, in *api.ListChannel return nil, status.Error(codes.InvalidArgument, "Invalid channel ID.") } - userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) messageList, err := ChannelMessagesList(s.logger, s.db, userID, streamConversionResult.Stream, in.ChannelId, limit, forward, in.Cursor) if err == ErrChannelCursorInvalid { return nil, status.Error(codes.InvalidArgument, "Cursor is invalid or expired.") @@ -55,5 +89,22 @@ func (s *ApiServer) ListChannelMessages(ctx context.Context, in *api.ListChannel return nil, status.Error(codes.Internal, "Error listing messages from channel.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterListChannelMessagesFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, messageList) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return messageList, nil } diff --git a/server/api_friend.go b/server/api_friend.go index 63c181542fee03254885b17ca3d6677cd777a730..36fbc7b0484f5bea80691409d21a6c1f02e15a28 100644 --- a/server/api_friend.go +++ b/server/api_friend.go @@ -15,32 +15,109 @@ package server import ( + "fmt" "github.com/gofrs/uuid" "github.com/golang/protobuf/ptypes/empty" "github.com/heroiclabs/nakama/api" + "go.opencensus.io/stats" + "go.opencensus.io/tag" + "go.opencensus.io/trace" "go.uber.org/zap" "golang.org/x/net/context" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "time" ) func (s *ApiServer) ListFriends(ctx context.Context, in *empty.Empty) (*api.Friends, error) { userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeListFriendsFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + friends, err := GetFriends(s.logger, s.db, s.tracker, userID) if err != nil { return nil, status.Error(codes.Internal, "Error while trying to list friends.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterListFriendsFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, friends) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return friends, nil } func (s *ApiServer) AddFriends(ctx context.Context, in *api.AddFriendsRequest) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeAddFriendsFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if len(in.GetIds()) == 0 && len(in.GetUsernames()) == 0 { return &empty.Empty{}, nil } - userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) username := ctx.Value(ctxUsernameKey{}).(string) for _, id := range in.GetIds() { @@ -76,15 +153,60 @@ func (s *ApiServer) AddFriends(ctx context.Context, in *api.AddFriendsRequest) ( return nil, status.Error(codes.Internal, "Error while trying to add friends.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterAddFriendsFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } func (s *ApiServer) DeleteFriends(ctx context.Context, in *api.DeleteFriendsRequest) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeDeleteFriendsFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if len(in.GetIds()) == 0 && len(in.GetUsernames()) == 0 { return &empty.Empty{}, nil } - userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) for _, id := range in.GetIds() { if userID.String() == id { return nil, status.Error(codes.InvalidArgument, "Cannot delete self.") @@ -120,15 +242,60 @@ func (s *ApiServer) DeleteFriends(ctx context.Context, in *api.DeleteFriendsRequ return nil, status.Error(codes.Internal, "Error while trying to delete friends.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterDeleteFriendsFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } func (s *ApiServer) BlockFriends(ctx context.Context, in *api.BlockFriendsRequest) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeBlockFriendsFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if len(in.GetIds()) == 0 && len(in.GetUsernames()) == 0 { return &empty.Empty{}, nil } - userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) for _, id := range in.GetIds() { if userID.String() == id { return nil, status.Error(codes.InvalidArgument, "Cannot block self.") @@ -163,10 +330,54 @@ func (s *ApiServer) BlockFriends(ctx context.Context, in *api.BlockFriendsReques return nil, status.Error(codes.Internal, "Error while trying to block friends.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterBlockFriendsFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } func (s *ApiServer) ImportFacebookFriends(ctx context.Context, in *api.ImportFacebookFriendsRequest) (*empty.Empty, error) { + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeImportFacebookFriendsFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, ctx.Value(ctxUserIDKey{}).(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", ctx.Value(ctxUserIDKey{}).(uuid.UUID).String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.Account == nil || in.Account.Token == "" { return nil, status.Error(codes.InvalidArgument, "Facebook token is required.") } @@ -177,5 +388,22 @@ func (s *ApiServer) ImportFacebookFriends(ctx context.Context, in *api.ImportFac return nil, err } + // After hook. + if fn := s.runtime.afterReqFunctions.afterImportFacebookFriendsFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, ctx.Value(ctxUserIDKey{}).(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } diff --git a/server/api_group.go b/server/api_group.go index 5b9bcd72181dcaaab6f6e37acd0f56d6ede88b69..dea450b71eac41e818f30fa4fa5f1403f954b73d 100644 --- a/server/api_group.go +++ b/server/api_group.go @@ -15,21 +15,54 @@ package server import ( + "fmt" "github.com/gofrs/uuid" "github.com/golang/protobuf/ptypes/empty" "github.com/heroiclabs/nakama/api" + "go.opencensus.io/stats" + "go.opencensus.io/tag" + "go.opencensus.io/trace" + "go.uber.org/zap" "golang.org/x/net/context" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "time" ) func (s *ApiServer) CreateGroup(ctx context.Context, in *api.CreateGroupRequest) (*api.Group, error) { + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeCreateGroupFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.GetName() == "" { return nil, status.Error(codes.InvalidArgument, "Group name must be set.") } - userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) - group, err := CreateGroup(s.logger, s.db, userID, userID, in.GetName(), in.GetLangTag(), in.GetDescription(), in.GetAvatarUrl(), "", in.GetOpen(), -1) if err != nil { if err == ErrGroupNameInUse { @@ -38,10 +71,56 @@ func (s *ApiServer) CreateGroup(ctx context.Context, in *api.CreateGroupRequest) return nil, status.Error(codes.Internal, "Error while trying to create group.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterCreateGroupFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, group) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return group, nil } func (s *ApiServer) UpdateGroup(ctx context.Context, in *api.UpdateGroupRequest) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeUpdateGroupFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.GetGroupId() == "" { return nil, status.Error(codes.InvalidArgument, "Group ID must be set.") } @@ -63,7 +142,6 @@ func (s *ApiServer) UpdateGroup(ctx context.Context, in *api.UpdateGroupRequest) } } - userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) err = UpdateGroup(s.logger, s.db, groupID, userID, nil, in.GetName(), in.GetLangTag(), in.GetDescription(), in.GetAvatarUrl(), nil, in.GetOpen(), -1) if err != nil { if err == ErrGroupPermissionDenied { @@ -76,10 +154,56 @@ func (s *ApiServer) UpdateGroup(ctx context.Context, in *api.UpdateGroupRequest) return nil, status.Error(codes.Internal, "Error while trying to update group.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterUpdateGroupFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } func (s *ApiServer) DeleteGroup(ctx context.Context, in *api.DeleteGroupRequest) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeDeleteGroupFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.GetGroupId() == "" { return nil, status.Error(codes.InvalidArgument, "Group ID must be set.") } @@ -89,7 +213,6 @@ func (s *ApiServer) DeleteGroup(ctx context.Context, in *api.DeleteGroupRequest) return nil, status.Error(codes.InvalidArgument, "Group ID must be a valid ID.") } - userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) err = DeleteGroup(s.logger, s.db, groupID, userID) if err != nil { if err == ErrGroupPermissionDenied { @@ -98,10 +221,56 @@ func (s *ApiServer) DeleteGroup(ctx context.Context, in *api.DeleteGroupRequest) return nil, status.Error(codes.Internal, "Error while trying to delete group.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterDeleteGroupFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } func (s *ApiServer) JoinGroup(ctx context.Context, in *api.JoinGroupRequest) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeJoinGroupFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.GetGroupId() == "" { return nil, status.Error(codes.InvalidArgument, "Group ID must be set.") } @@ -111,8 +280,6 @@ func (s *ApiServer) JoinGroup(ctx context.Context, in *api.JoinGroupRequest) (*e return nil, status.Error(codes.InvalidArgument, "Group ID must be a valid ID.") } - userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) - err = JoinGroup(s.logger, s.db, groupID, userID) if err != nil { if err == ErrGroupNotFound { @@ -123,10 +290,56 @@ func (s *ApiServer) JoinGroup(ctx context.Context, in *api.JoinGroupRequest) (*e return nil, status.Error(codes.Internal, "Error while trying to join group.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterJoinGroupFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } func (s *ApiServer) LeaveGroup(ctx context.Context, in *api.LeaveGroupRequest) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeLeaveGroupFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.GetGroupId() == "" { return nil, status.Error(codes.InvalidArgument, "Group ID must be set.") } @@ -136,7 +349,6 @@ func (s *ApiServer) LeaveGroup(ctx context.Context, in *api.LeaveGroupRequest) ( return nil, status.Error(codes.InvalidArgument, "Group ID must be a valid ID.") } - userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) err = LeaveGroup(s.logger, s.db, groupID, userID) if err != nil { if err == ErrGroupLastSuperadmin { @@ -145,10 +357,56 @@ func (s *ApiServer) LeaveGroup(ctx context.Context, in *api.LeaveGroupRequest) ( return nil, status.Error(codes.Internal, "Error while trying to leave group.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterLeaveGroupFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } func (s *ApiServer) AddGroupUsers(ctx context.Context, in *api.AddGroupUsersRequest) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeAddGroupUsersFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.GetGroupId() == "" { return nil, status.Error(codes.InvalidArgument, "Group ID must be set.") } @@ -171,7 +429,6 @@ func (s *ApiServer) AddGroupUsers(ctx context.Context, in *api.AddGroupUsersRequ userIDs = append(userIDs, uid) } - userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) err = AddGroupUsers(s.logger, s.db, userID, groupID, userIDs) if err != nil { if err == ErrGroupPermissionDenied { @@ -182,10 +439,56 @@ func (s *ApiServer) AddGroupUsers(ctx context.Context, in *api.AddGroupUsersRequ return nil, status.Error(codes.Internal, "Error while trying to add users to a group.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterAddGroupUsersFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } func (s *ApiServer) KickGroupUsers(ctx context.Context, in *api.KickGroupUsersRequest) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeKickGroupUsersFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.GetGroupId() == "" { return nil, status.Error(codes.InvalidArgument, "Group ID must be set.") } @@ -208,7 +511,6 @@ func (s *ApiServer) KickGroupUsers(ctx context.Context, in *api.KickGroupUsersRe userIDs = append(userIDs, uid) } - userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) if err = KickGroupUsers(s.logger, s.db, userID, groupID, userIDs); err != nil { if err == ErrGroupPermissionDenied { return nil, status.Error(codes.NotFound, "Group not found or permission denied.") @@ -216,10 +518,56 @@ func (s *ApiServer) KickGroupUsers(ctx context.Context, in *api.KickGroupUsersRe return nil, status.Error(codes.Internal, "Error while trying to kick users from a group.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterKickGroupUsersFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } func (s *ApiServer) PromoteGroupUsers(ctx context.Context, in *api.PromoteGroupUsersRequest) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforePromoteGroupUsersFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.GetGroupId() == "" { return nil, status.Error(codes.InvalidArgument, "Group ID must be set.") } @@ -242,7 +590,6 @@ func (s *ApiServer) PromoteGroupUsers(ctx context.Context, in *api.PromoteGroupU userIDs = append(userIDs, uid) } - userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) err = PromoteGroupUsers(s.logger, s.db, userID, groupID, userIDs) if err != nil { if err == ErrGroupPermissionDenied { @@ -251,10 +598,54 @@ func (s *ApiServer) PromoteGroupUsers(ctx context.Context, in *api.PromoteGroupU return nil, status.Error(codes.Internal, "Error while trying to promote users in a group.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterPromoteGroupUsersFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } func (s *ApiServer) ListGroupUsers(ctx context.Context, in *api.ListGroupUsersRequest) (*api.GroupUserList, error) { + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeListGroupUsersFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, ctx.Value(ctxUserIDKey{}).(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", ctx.Value(ctxUserIDKey{}).(uuid.UUID).String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.GetGroupId() == "" { return nil, status.Error(codes.InvalidArgument, "Group ID must be set.") } @@ -269,10 +660,54 @@ func (s *ApiServer) ListGroupUsers(ctx context.Context, in *api.ListGroupUsersRe return nil, status.Error(codes.Internal, "Error while trying to list users in a group.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterListGroupUsersFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, ctx.Value(ctxUserIDKey{}).(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, groupUsers) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return groupUsers, nil } func (s *ApiServer) ListUserGroups(ctx context.Context, in *api.ListUserGroupsRequest) (*api.UserGroupList, error) { + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeListUserGroupsFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, ctx.Value(ctxUserIDKey{}).(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", ctx.Value(ctxUserIDKey{}).(uuid.UUID).String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.GetUserId() == "" { return nil, status.Error(codes.InvalidArgument, "User ID must be set.") } @@ -287,10 +722,54 @@ func (s *ApiServer) ListUserGroups(ctx context.Context, in *api.ListUserGroupsRe return nil, status.Error(codes.Internal, "Error while trying to list groups for a user.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterListUserGroupsFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, ctx.Value(ctxUserIDKey{}).(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, userGroups) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return userGroups, nil } func (s *ApiServer) ListGroups(ctx context.Context, in *api.ListGroupsRequest) (*api.GroupList, error) { + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeListGroupsFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, ctx.Value(ctxUserIDKey{}).(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", ctx.Value(ctxUserIDKey{}).(uuid.UUID).String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + limit := 1 if in.GetLimit() != nil { if in.GetLimit().Value < 1 || in.GetLimit().Value > 100 { @@ -304,5 +783,22 @@ func (s *ApiServer) ListGroups(ctx context.Context, in *api.ListGroupsRequest) ( return nil, status.Error(codes.Internal, "Error while trying to list groups.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterListGroupsFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, ctx.Value(ctxUserIDKey{}).(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, groups) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return groups, nil } diff --git a/server/api_leaderboard.go b/server/api_leaderboard.go index ffa5ae3e24cb5ecc7f9d76cf0afd5539d5dc3053..a114483872bfd7c024d2eefac6b18efc85ae42ff 100644 --- a/server/api_leaderboard.go +++ b/server/api_leaderboard.go @@ -16,22 +16,55 @@ package server import ( "encoding/json" + "fmt" "github.com/gofrs/uuid" "github.com/golang/protobuf/ptypes/empty" "github.com/golang/protobuf/ptypes/wrappers" "github.com/heroiclabs/nakama/api" + "go.opencensus.io/stats" + "go.opencensus.io/tag" + "go.opencensus.io/trace" + "go.uber.org/zap" "golang.org/x/net/context" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "time" ) func (s *ApiServer) DeleteLeaderboardRecord(ctx context.Context, in *api.DeleteLeaderboardRecordRequest) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeDeleteLeaderboardRecordFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.LeaderboardId == "" { return nil, status.Error(codes.InvalidArgument, "Invalid leaderboard ID.") } - userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) - err := LeaderboardRecordDelete(s.logger, s.db, s.leaderboardCache, userID, in.LeaderboardId, userID.String()) if err == ErrLeaderboardNotFound { return nil, status.Error(codes.NotFound, "Leaderboard not found.") @@ -41,10 +74,54 @@ func (s *ApiServer) DeleteLeaderboardRecord(ctx context.Context, in *api.DeleteL return nil, status.Error(codes.Internal, "Error deleting score from leaderboard.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterDeleteLeaderboardRecordFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } func (s *ApiServer) ListLeaderboardRecords(ctx context.Context, in *api.ListLeaderboardRecordsRequest) (*api.LeaderboardRecordList, error) { + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeListLeaderboardRecordsFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, ctx.Value(ctxUserIDKey{}).(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", ctx.Value(ctxUserIDKey{}).(uuid.UUID).String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.LeaderboardId == "" { return nil, status.Error(codes.InvalidArgument, "Invalid leaderboard ID.") } @@ -76,10 +153,57 @@ func (s *ApiServer) ListLeaderboardRecords(ctx context.Context, in *api.ListLead return nil, status.Error(codes.Internal, "Error listing records from leaderboard.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterListLeaderboardRecordsFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, ctx.Value(ctxUserIDKey{}).(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, records) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return records, nil } func (s *ApiServer) WriteLeaderboardRecord(ctx context.Context, in *api.WriteLeaderboardRecordRequest) (*api.LeaderboardRecord, error) { + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) + username := ctx.Value(ctxUsernameKey{}).(string) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeWriteLeaderboardRecordFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.String(), username, ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.LeaderboardId == "" { return nil, status.Error(codes.InvalidArgument, "Invalid leaderboard ID.") } else if in.Record == nil { @@ -95,9 +219,6 @@ func (s *ApiServer) WriteLeaderboardRecord(ctx context.Context, in *api.WriteLea } } - userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) - username := ctx.Value(ctxUsernameKey{}).(string) - record, err := LeaderboardRecordWrite(s.logger, s.db, s.leaderboardCache, userID, in.LeaderboardId, userID.String(), username, in.Record.Score, in.Record.Subscore, in.Record.Metadata) if err == ErrLeaderboardNotFound { return nil, status.Error(codes.NotFound, "Leaderboard not found.") @@ -107,5 +228,22 @@ func (s *ApiServer) WriteLeaderboardRecord(ctx context.Context, in *api.WriteLea return nil, status.Error(codes.Internal, "Error writing score to leaderboard.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterWriteLeaderboardRecordFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, record) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return record, nil } diff --git a/server/api_link.go b/server/api_link.go index 96e90f8d080813e8b9ea7acf9c2c8f45cb530f48..47838dd7391a36b2385bba62e479fac22aa75f36 100644 --- a/server/api_link.go +++ b/server/api_link.go @@ -15,8 +15,13 @@ package server import ( + "fmt" + "go.opencensus.io/stats" + "go.opencensus.io/tag" + "go.opencensus.io/trace" "strconv" "strings" + "time" "github.com/cockroachdb/cockroach-go/crdb" "github.com/gofrs/uuid" @@ -31,6 +36,35 @@ import ( ) func (s *ApiServer) LinkCustom(ctx context.Context, in *api.AccountCustom) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeLinkCustomFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.(uuid.UUID).String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + customID := in.Id if customID == "" { return nil, status.Error(codes.InvalidArgument, "Custom ID is required.") @@ -40,7 +74,6 @@ func (s *ApiServer) LinkCustom(ctx context.Context, in *api.AccountCustom) (*emp return nil, status.Error(codes.InvalidArgument, "Invalid custom ID, must be 6-128 bytes.") } - userID := ctx.Value(ctxUserIDKey{}) res, err := s.db.Exec(` UPDATE users SET custom_id = $2, update_time = now() @@ -59,10 +92,56 @@ AND (NOT EXISTS return nil, status.Error(codes.AlreadyExists, "Custom ID is already in use.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterLinkCustomFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } func (s *ApiServer) LinkDevice(ctx context.Context, in *api.AccountDevice) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeLinkDeviceFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.(uuid.UUID).String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + deviceID := in.Id if deviceID == "" { return nil, status.Error(codes.InvalidArgument, "Device ID is required.") @@ -79,8 +158,6 @@ func (s *ApiServer) LinkDevice(ctx context.Context, in *api.AccountDevice) (*emp } err = crdb.ExecuteInTx(ctx, tx, func() error { - userID := ctx.Value(ctxUserIDKey{}) - var dbDeviceIdLinkedUser int64 err := tx.QueryRow("SELECT COUNT(id) FROM user_device WHERE id = $1 AND user_id = $2 LIMIT 1", deviceID, userID).Scan(&dbDeviceIdLinkedUser) if err != nil { @@ -115,10 +192,56 @@ func (s *ApiServer) LinkDevice(ctx context.Context, in *api.AccountDevice) (*emp return nil, status.Error(codes.Internal, "Error linking Device ID.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterLinkDeviceFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } func (s *ApiServer) LinkEmail(ctx context.Context, in *api.AccountEmail) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeLinkEmailFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.(uuid.UUID).String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.Email == "" || in.Password == "" { return nil, status.Error(codes.InvalidArgument, "Email address and password is required.") } else if invalidCharsRegex.MatchString(in.Email) { @@ -134,7 +257,6 @@ func (s *ApiServer) LinkEmail(ctx context.Context, in *api.AccountEmail) (*empty cleanEmail := strings.ToLower(in.Email) hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost) - userID := ctx.Value(ctxUserIDKey{}) res, err := s.db.Exec(` UPDATE users SET email = $2, password = $3, update_time = now() @@ -154,10 +276,56 @@ AND (NOT EXISTS return nil, status.Error(codes.AlreadyExists, "Email is already in use.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterLinkEmailFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } func (s *ApiServer) LinkFacebook(ctx context.Context, in *api.LinkFacebookRequest) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeLinkFacebookFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.(uuid.UUID).String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.Account == nil || in.Account.Token == "" { return nil, status.Error(codes.InvalidArgument, "Facebook access token is required.") } @@ -168,7 +336,6 @@ func (s *ApiServer) LinkFacebook(ctx context.Context, in *api.LinkFacebookReques return nil, status.Error(codes.Unauthenticated, "Could not authenticate Facebook profile.") } - userID := ctx.Value(ctxUserIDKey{}) res, err := s.db.Exec(` UPDATE users SET facebook_id = $2, update_time = now() @@ -192,10 +359,56 @@ AND (NOT EXISTS importFacebookFriends(s.logger, s.db, s.router, s.socialClient, userID.(uuid.UUID), ctx.Value(ctxUsernameKey{}).(string), in.Account.Token, false) } + // After hook. + if fn := s.runtime.afterReqFunctions.afterLinkFacebookFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } func (s *ApiServer) LinkGameCenter(ctx context.Context, in *api.AccountGameCenter) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeLinkGameCenterFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.(uuid.UUID).String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.BundleId == "" { return nil, status.Error(codes.InvalidArgument, "GameCenter bundle ID is required.") } else if in.PlayerId == "" { @@ -216,7 +429,6 @@ func (s *ApiServer) LinkGameCenter(ctx context.Context, in *api.AccountGameCente return nil, status.Error(codes.Unauthenticated, "Could not authenticate GameCenter profile.") } - userID := ctx.Value(ctxUserIDKey{}) res, err := s.db.Exec(` UPDATE users SET gamecenter_id = $2, update_time = now() @@ -235,10 +447,56 @@ AND (NOT EXISTS return nil, status.Error(codes.AlreadyExists, "GameCenter ID is already in use.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterLinkGameCenterFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } func (s *ApiServer) LinkGoogle(ctx context.Context, in *api.AccountGoogle) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeLinkGoogleFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.(uuid.UUID).String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.Token == "" { return nil, status.Error(codes.InvalidArgument, "Google access token is required.") } @@ -249,7 +507,6 @@ func (s *ApiServer) LinkGoogle(ctx context.Context, in *api.AccountGoogle) (*emp return nil, status.Error(codes.Unauthenticated, "Could not authenticate Google profile.") } - userID := ctx.Value(ctxUserIDKey{}) res, err := s.db.Exec(` UPDATE users SET google_id = $2, update_time = now() @@ -268,10 +525,56 @@ AND (NOT EXISTS return nil, status.Error(codes.AlreadyExists, "Google ID is already in use.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterLinkGoogleFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } func (s *ApiServer) LinkSteam(ctx context.Context, in *api.AccountSteam) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeLinkSteamFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.(uuid.UUID).String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if s.config.GetSocial().Steam.PublisherKey == "" || s.config.GetSocial().Steam.AppID == 0 { return nil, status.Error(codes.FailedPrecondition, "Steam authentication is not configured.") } @@ -286,7 +589,6 @@ func (s *ApiServer) LinkSteam(ctx context.Context, in *api.AccountSteam) (*empty return nil, status.Error(codes.Unauthenticated, "Could not authenticate Steam profile.") } - userID := ctx.Value(ctxUserIDKey{}) res, err := s.db.Exec(` UPDATE users SET steam_id = $2, update_time = now() @@ -305,5 +607,22 @@ AND (NOT EXISTS return nil, status.Error(codes.AlreadyExists, "Steam ID is already in use.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterLinkSteamFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } diff --git a/server/api_match.go b/server/api_match.go index e96467054508ce1f5c448cead497f445346a57db..2395407b934573e1ba90866b17a1204d7c3e25a6 100644 --- a/server/api_match.go +++ b/server/api_match.go @@ -15,13 +15,47 @@ package server import ( + "fmt" + "github.com/gofrs/uuid" "github.com/heroiclabs/nakama/api" + "go.opencensus.io/stats" + "go.opencensus.io/tag" + "go.opencensus.io/trace" + "go.uber.org/zap" "golang.org/x/net/context" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "time" ) func (s *ApiServer) ListMatches(ctx context.Context, in *api.ListMatchesRequest) (*api.MatchList, error) { + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeListMatchesFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, ctx.Value(ctxUserIDKey{}).(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", ctx.Value(ctxUserIDKey{}).(uuid.UUID).String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + limit := 1 if in.GetLimit() != nil { if in.GetLimit().Value < 1 || in.GetLimit().Value > 100 { @@ -46,5 +80,24 @@ func (s *ApiServer) ListMatches(ctx context.Context, in *api.ListMatchesRequest) results := s.matchRegistry.ListMatches(limit, in.Authoritative, in.Label, in.MinSize, in.MaxSize) - return &api.MatchList{Matches: results}, nil + list := &api.MatchList{Matches: results} + + // After hook. + if fn := s.runtime.afterReqFunctions.afterListMatchesFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, ctx.Value(ctxUserIDKey{}).(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, list) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + + return list, nil } diff --git a/server/api_notification.go b/server/api_notification.go index 80d832584316250ee005f3c39f04f87ad06a669e..65141b304baca78b1f81f7d7db0a7a7d47721523 100644 --- a/server/api_notification.go +++ b/server/api_notification.go @@ -19,6 +19,11 @@ import ( "context" "encoding/base64" "encoding/gob" + "fmt" + "go.opencensus.io/stats" + "go.opencensus.io/tag" + "go.opencensus.io/trace" + "time" "github.com/gofrs/uuid" "github.com/golang/protobuf/ptypes/empty" @@ -29,6 +34,35 @@ import ( ) func (s *ApiServer) ListNotifications(ctx context.Context, in *api.ListNotificationsRequest) (*api.NotificationList, error) { + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeListNotificationsFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + limit := 1 if in.GetLimit() != nil { if in.GetLimit().Value < 1 || in.GetLimit().Value > 100 { @@ -52,24 +86,85 @@ func (s *ApiServer) ListNotifications(ctx context.Context, in *api.ListNotificat } } - userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) notificationList, err := NotificationList(s.logger, s.db, userID, limit, cursor, nc) if err != nil { return nil, status.Error(codes.Internal, "Error retrieving notifications.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterListNotificationsFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, notificationList) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return notificationList, nil } func (s *ApiServer) DeleteNotifications(ctx context.Context, in *api.DeleteNotificationsRequest) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeDeleteNotificationFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, ctx.Value(ctxUserIDKey{}).(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if len(in.GetIds()) == 0 { return &empty.Empty{}, nil } - userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) if err := NotificationDelete(s.logger, s.db, userID, in.GetIds()); err != nil { return nil, status.Error(codes.Internal, "Error while deleting notifications.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterDeleteNotificationFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, ctx.Value(ctxUserIDKey{}).(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } diff --git a/server/api_rpc.go b/server/api_rpc.go index 2e69cc31ae948a4a0ae0397f0535f440466abae9..72a8ef23803c22e00bc18928fd61f6196de52d97 100644 --- a/server/api_rpc.go +++ b/server/api_rpc.go @@ -20,9 +20,7 @@ import ( "github.com/gofrs/uuid" "github.com/heroiclabs/nakama/api" - "github.com/yuin/gopher-lua" "go.uber.org/zap" - "go.uber.org/zap/zapcore" "golang.org/x/net/context" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" @@ -37,7 +35,8 @@ func (s *ApiServer) RpcFunc(ctx context.Context, in *api.Rpc) (*api.Rpc, error) id := strings.ToLower(in.Id) - if !s.runtimePool.HasCallback(ExecutionModeRPC, id) { + fn := s.runtime.Rpc(id) + if fn == nil { return nil, status.Error(codes.NotFound, "RPC function not found") } @@ -65,14 +64,6 @@ func (s *ApiServer) RpcFunc(ctx context.Context, in *api.Rpc) (*api.Rpc, error) if e := ctx.Value(ctxExpiryKey{}); e != nil { expiry = e.(int64) } - - runtime := s.runtimePool.Get() - lf := runtime.GetCallback(ExecutionModeRPC, id) - if lf == nil { - s.runtimePool.Put(runtime) - return nil, status.Error(codes.NotFound, "RPC function not found") - } - clientAddr := "" clientIP := "" clientPort := "" @@ -84,7 +75,6 @@ func (s *ApiServer) RpcFunc(ctx context.Context, in *api.Rpc) (*api.Rpc, error) // if missing, try to look up gRPC peer info clientAddr = peerInfo.Addr.String() } - clientAddr = strings.TrimSpace(clientAddr) if host, port, err := net.SplitHostPort(clientAddr); err == nil { clientIP = host @@ -95,36 +85,10 @@ func (s *ApiServer) RpcFunc(ctx context.Context, in *api.Rpc) (*api.Rpc, error) s.logger.Debug("Could not extract client address from request.", zap.Error(err)) } - result, fnErr, code := runtime.InvokeFunction(ExecutionModeRPC, lf, queryParams, uid, username, expiry, "", clientIP, clientPort, in.Payload) - s.runtimePool.Put(runtime) - + result, fnErr, code := fn(queryParams, uid, username, expiry, "", clientIP, clientPort, in.Payload) if fnErr != nil { - s.logger.Error("Runtime RPC function caused an error", zap.String("id", in.Id), zap.Error(fnErr)) - if apiErr, ok := fnErr.(*lua.ApiError); ok && !s.logger.Core().Enabled(zapcore.InfoLevel) { - msg := apiErr.Object.String() - if strings.HasPrefix(msg, lf.Proto.SourceName) { - msg = msg[len(lf.Proto.SourceName):] - msgParts := strings.SplitN(msg, ": ", 2) - if len(msgParts) == 2 { - msg = msgParts[1] - } else { - msg = msgParts[0] - } - } - return nil, status.Error(code, msg) - } else { - return nil, status.Error(code, fnErr.Error()) - } - } - - if result == nil { - return &api.Rpc{}, nil + return nil, status.Error(code, fnErr.Error()) } - if payload, ok := result.(string); !ok { - s.logger.Warn("Runtime function returned invalid data", zap.Any("result", result)) - return nil, status.Error(codes.Internal, "Runtime function returned invalid data - only allowed one return value of type String/Byte.") - } else { - return &api.Rpc{Payload: payload}, nil - } + return &api.Rpc{Payload: result}, nil } diff --git a/server/api_storage.go b/server/api_storage.go index a2a9de09f30f2591e4f641ad0538b0e10c3db8c4..d88ce1b3205db0782fe8d22fe870e3c7a9d27b8b 100644 --- a/server/api_storage.go +++ b/server/api_storage.go @@ -16,6 +16,12 @@ package server import ( "encoding/json" + "fmt" + "go.opencensus.io/stats" + "go.opencensus.io/tag" + "go.opencensus.io/trace" + "go.uber.org/zap" + "time" "github.com/gofrs/uuid" "github.com/golang/protobuf/ptypes/empty" @@ -26,6 +32,35 @@ import ( ) func (s *ApiServer) ListStorageObjects(ctx context.Context, in *api.ListStorageObjectsRequest) (*api.StorageObjectList, error) { + caller := ctx.Value(ctxUserIDKey{}).(uuid.UUID) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeListStorageObjectsFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, caller.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", caller.String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + limit := 1 if in.GetLimit() != nil { if in.GetLimit().Value < 1 || in.GetLimit().Value > 100 { @@ -34,7 +69,6 @@ func (s *ApiServer) ListStorageObjects(ctx context.Context, in *api.ListStorageO limit = int(in.GetLimit().Value) } - caller := ctx.Value(ctxUserIDKey{}).(uuid.UUID) userID := uuid.Nil if in.GetUserId() != "" { uid, err := uuid.FromString(in.GetUserId()) @@ -53,10 +87,56 @@ func (s *ApiServer) ListStorageObjects(ctx context.Context, in *api.ListStorageO return nil, status.Error(code, listingError.Error()) } + // After hook. + if fn := s.runtime.afterReqFunctions.afterListStorageObjectsFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, caller.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, storageObjectList) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return storageObjectList, nil } func (s *ApiServer) ReadStorageObjects(ctx context.Context, in *api.ReadStorageObjectsRequest) (*api.StorageObjects, error) { + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeReadStorageObjectsFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.GetObjectIds() == nil || len(in.GetObjectIds()) == 0 { return &api.StorageObjects{}, nil } @@ -73,17 +153,61 @@ func (s *ApiServer) ReadStorageObjects(ctx context.Context, in *api.ReadStorageO } } - userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) - objects, err := StorageReadObjects(s.logger, s.db, userID, in.GetObjectIds()) if err != nil { return nil, status.Error(codes.Internal, "Error reading storage objects.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterReadStorageObjectsFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, ctx.Value(ctxUserIDKey{}).(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, objects) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return objects, nil } func (s *ApiServer) WriteStorageObjects(ctx context.Context, in *api.WriteStorageObjectsRequest) (*api.StorageObjectAcks, error) { + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeWriteStorageObjectsFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", ctx.Value(ctxUsernameKey{}).(string))) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.GetObjects() == nil || len(in.GetObjects()) == 0 { return &api.StorageObjectAcks{}, nil } @@ -113,21 +237,66 @@ func (s *ApiServer) WriteStorageObjects(ctx context.Context, in *api.WriteStorag } } - userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) userObjects := map[uuid.UUID][]*api.WriteStorageObject{userID: in.GetObjects()} acks, code, err := StorageWriteObjects(s.logger, s.db, false, userObjects) - if err == nil { - return acks, nil + if err != nil { + if code == codes.Internal { + return nil, status.Error(codes.Internal, "Error writing storage objects.") + } + return nil, status.Error(code, err.Error()) } - if code == codes.Internal { - return nil, status.Error(codes.Internal, "Error writing storage objects.") + // After hook. + if fn := s.runtime.afterReqFunctions.afterWriteStorageObjectsFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, acks) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) } - return nil, status.Error(code, err.Error()) + + return acks, nil } func (s *ApiServer) DeleteStorageObjects(ctx context.Context, in *api.DeleteStorageObjectsRequest) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeDeleteStorageObjectsFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.GetObjectIds() == nil || len(in.GetObjectIds()) == 0 { return &empty.Empty{}, nil } @@ -138,7 +307,6 @@ func (s *ApiServer) DeleteStorageObjects(ctx context.Context, in *api.DeleteStor } } - userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) objectIDs := map[uuid.UUID][]*api.DeleteStorageObjectId{userID: in.GetObjectIds()} if code, err := StorageDeleteObjects(s.logger, s.db, false, objectIDs); err != nil { @@ -148,5 +316,22 @@ func (s *ApiServer) DeleteStorageObjects(ctx context.Context, in *api.DeleteStor return nil, status.Error(code, err.Error()) } + // After hook. + if fn := s.runtime.afterReqFunctions.afterDeleteStorageObjectsFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, ctx.Value(ctxUserIDKey{}).(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } diff --git a/server/api_unlink.go b/server/api_unlink.go index 35486f92df04d4135492da94eb9142e10363692f..291e98a0eec23658b3eedbed04b052aa8c5fa5cc 100644 --- a/server/api_unlink.go +++ b/server/api_unlink.go @@ -15,8 +15,14 @@ package server import ( + "fmt" + "github.com/gofrs/uuid" + "go.opencensus.io/stats" + "go.opencensus.io/tag" + "go.opencensus.io/trace" "strconv" "strings" + "time" "github.com/cockroachdb/cockroach-go/crdb" "github.com/golang/protobuf/ptypes/empty" @@ -28,6 +34,35 @@ import ( ) func (s *ApiServer) UnlinkCustom(ctx context.Context, in *api.AccountCustom) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeUnlinkCustomFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.(uuid.UUID).String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.GetId() == "" { return nil, status.Error(codes.InvalidArgument, "An ID must be supplied.") } @@ -43,7 +78,6 @@ AND ((facebook_id IS NOT NULL OR EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))` - userID := ctx.Value(ctxUserIDKey{}) res, err := s.db.Exec(query, userID, in.Id) if err != nil { @@ -53,10 +87,56 @@ AND ((facebook_id IS NOT NULL return nil, status.Error(codes.PermissionDenied, "Cannot unlink last account identifier. Check profile exists and is not last link.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterUnlinkCustomFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } func (s *ApiServer) UnlinkDevice(ctx context.Context, in *api.AccountDevice) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeUnlinkDeviceFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.(uuid.UUID).String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.GetId() == "" { return nil, status.Error(codes.InvalidArgument, "A device ID must be supplied.") } @@ -68,8 +148,6 @@ func (s *ApiServer) UnlinkDevice(ctx context.Context, in *api.AccountDevice) (*e } err = crdb.ExecuteInTx(ctx, tx, func() error { - userID := ctx.Value(ctxUserIDKey{}) - query := `DELETE FROM user_device WHERE id = $2 AND user_id = $1 AND (EXISTS (SELECT id FROM users WHERE id = $1 AND (facebook_id IS NOT NULL @@ -109,10 +187,56 @@ AND (EXISTS (SELECT id FROM users WHERE id = $1 AND return nil, status.Error(codes.Internal, "Could not unlink device ID.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterUnlinkDeviceFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } func (s *ApiServer) UnlinkEmail(ctx context.Context, in *api.AccountEmail) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeUnlinkEmailFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.(uuid.UUID).String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.GetEmail() == "" || in.GetPassword() == "" { return nil, status.Error(codes.InvalidArgument, "Both email and password must be supplied.") } @@ -128,7 +252,6 @@ AND ((facebook_id IS NOT NULL OR EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))` - userID := ctx.Value(ctxUserIDKey{}) cleanEmail := strings.ToLower(in.Email) res, err := s.db.Exec(query, userID, cleanEmail) @@ -139,10 +262,56 @@ AND ((facebook_id IS NOT NULL return nil, status.Error(codes.PermissionDenied, "Cannot unlink last account identifier. Check profile exists and is not last link.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterUnlinkEmailFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } func (s *ApiServer) UnlinkFacebook(ctx context.Context, in *api.AccountFacebook) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeUnlinkFacebookFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.(uuid.UUID).String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.Token == "" { return nil, status.Error(codes.InvalidArgument, "Facebook access token is required.") } @@ -164,7 +333,6 @@ AND ((custom_id IS NOT NULL OR EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))` - userID := ctx.Value(ctxUserIDKey{}) res, err := s.db.Exec(query, userID, facebookProfile.ID) if err != nil { @@ -174,10 +342,56 @@ AND ((custom_id IS NOT NULL return nil, status.Error(codes.PermissionDenied, "Cannot unlink last account identifier. Check profile exists and is not last link.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterUnlinkFacebookFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } func (s *ApiServer) UnlinkGameCenter(ctx context.Context, in *api.AccountGameCenter) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeUnlinkGameCenterFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.(uuid.UUID).String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.BundleId == "" { return nil, status.Error(codes.InvalidArgument, "GameCenter bundle ID is required.") } else if in.PlayerId == "" { @@ -209,7 +423,6 @@ AND ((custom_id IS NOT NULL OR EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))` - userID := ctx.Value(ctxUserIDKey{}) res, err := s.db.Exec(query, userID, in.PlayerId) if err != nil { @@ -219,10 +432,56 @@ AND ((custom_id IS NOT NULL return nil, status.Error(codes.PermissionDenied, "Cannot unlink last account identifier. Check profile exists and is not last link.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterUnlinkGameCenterFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } func (s *ApiServer) UnlinkGoogle(ctx context.Context, in *api.AccountGoogle) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeUnlinkGoogleFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.(uuid.UUID).String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.Token == "" { return nil, status.Error(codes.InvalidArgument, "Google access token is required.") } @@ -244,7 +503,6 @@ AND ((custom_id IS NOT NULL OR EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))` - userID := ctx.Value(ctxUserIDKey{}) res, err := s.db.Exec(query, userID, googleProfile.Sub) if err != nil { @@ -254,10 +512,56 @@ AND ((custom_id IS NOT NULL return nil, status.Error(codes.PermissionDenied, "Cannot unlink last account identifier. Check profile exists and is not last link.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterUnlinkGoogleFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } func (s *ApiServer) UnlinkSteam(ctx context.Context, in *api.AccountSteam) (*empty.Empty, error) { + userID := ctx.Value(ctxUserIDKey{}) + + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeUnlinkSteamFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", userID.(uuid.UUID).String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if s.config.GetSocial().Steam.PublisherKey == "" || s.config.GetSocial().Steam.AppID == 0 { return nil, status.Error(codes.FailedPrecondition, "Steam authentication is not configured.") } @@ -283,7 +587,6 @@ AND ((custom_id IS NOT NULL OR EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))` - userID := ctx.Value(ctxUserIDKey{}) res, err := s.db.Exec(query, userID, strconv.FormatUint(steamProfile.SteamID, 10)) if err != nil { @@ -293,5 +596,22 @@ AND ((custom_id IS NOT NULL return nil, status.Error(codes.PermissionDenied, "Cannot unlink last account identifier. Check profile exists and is not last link.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterUnlinkSteamFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, &empty.Empty{}) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return &empty.Empty{}, nil } diff --git a/server/api_user.go b/server/api_user.go index f36745060389fa5f6e0f9658970d6acf39db2b58..454d33c559015a5ec6948811f94f6fa16ff32efd 100644 --- a/server/api_user.go +++ b/server/api_user.go @@ -15,14 +15,47 @@ package server import ( + "fmt" "github.com/gofrs/uuid" "github.com/heroiclabs/nakama/api" + "go.opencensus.io/stats" + "go.opencensus.io/tag" + "go.opencensus.io/trace" + "go.uber.org/zap" "golang.org/x/net/context" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "time" ) func (s *ApiServer) GetUsers(ctx context.Context, in *api.GetUsersRequest) (*api.Users, error) { + // Before hook. + if fn := s.runtime.beforeReqFunctions.beforeGetUsersFunction; fn != nil { + // Stats measurement start boundary. + fullMethod := ctx.Value(ctxFullMethodKey{}).(string) + name := fmt.Sprintf("%v-before", fullMethod) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + result, err, code := fn(s.logger, ctx.Value(ctxUserIDKey{}).(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + if err != nil { + return nil, status.Error(code, err.Error()) + } + if result == nil { + // If result is nil, requested resource is disabled. + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", fullMethod), zap.String("uid", ctx.Value(ctxUserIDKey{}).(uuid.UUID).String())) + return nil, status.Error(codes.NotFound, "Requested resource was not found.") + } + in = result + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + if in.GetIds() == nil && in.GetUsernames() == nil && in.GetFacebookIds() == nil { return &api.Users{}, nil } @@ -54,5 +87,22 @@ func (s *ApiServer) GetUsers(ctx context.Context, in *api.GetUsersRequest) (*api return nil, status.Error(codes.Internal, "Error retrieving user accounts.") } + // After hook. + if fn := s.runtime.afterReqFunctions.afterGetUsersFunction; fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("%v-after", ctx.Value(ctxFullMethodKey{}).(string)) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Extract request information and execute the hook. + clientIP, clientPort := extractClientAddress(s.logger, ctx) + fn(s.logger, ctx.Value(ctxUserIDKey{}).(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, users) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsApiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsApiCount.M(1)) + } + return users, nil } diff --git a/server/config.go b/server/config.go index e15568d1cca45f4506970959682258945defd220..140c694568f564f167b6e627c2ca6d017160248d 100644 --- a/server/config.go +++ b/server/config.go @@ -181,8 +181,8 @@ func ParseArgs(logger *zap.Logger, args []string) Config { return mainConfig } -func convertRuntimeEnv(logger *zap.Logger, existingEnv map[string]interface{}, mergeEnv []string) map[string]interface{} { - envMap := make(map[string]interface{}, len(existingEnv)) +func convertRuntimeEnv(logger *zap.Logger, existingEnv map[string]string, mergeEnv []string) map[string]string { + envMap := make(map[string]string, len(existingEnv)) for k, v := range existingEnv { envMap[k] = v } @@ -412,7 +412,7 @@ func NewSocialConfig() *SocialConfig { // RuntimeConfig is configuration relevant to the Runtime Lua VM. type RuntimeConfig struct { - Environment map[string]interface{} + Environment map[string]string Env []string `yaml:"env" json:"env"` Path string `yaml:"path" json:"path" usage:"Path for the server to scan for *.lua files."` HTTPKey string `yaml:"http_key" json:"http_key" usage:"Runtime HTTP Invocation key."` @@ -425,7 +425,7 @@ type RuntimeConfig struct { // NewRuntimeConfig creates a new RuntimeConfig struct. func NewRuntimeConfig() *RuntimeConfig { return &RuntimeConfig{ - Environment: make(map[string]interface{}, 0), + Environment: make(map[string]string, 0), Env: make([]string, 0), Path: "", HTTPKey: "defaultkey", diff --git a/server/core_group.go b/server/core_group.go index 2c9a08e4db93ef9e2074e1697f270606f1c088c3..9be597dc59b4864d79bac6ce58d4c33a53e127a7 100644 --- a/server/core_group.go +++ b/server/core_group.go @@ -218,7 +218,6 @@ func UpdateGroup(logger *zap.Logger, db *sql.DB, groupID uuid.UUID, userID uuid. if creatorID != nil { statements = append(statements, "creator_id = $"+strconv.Itoa(index)+"::UUID") params = append(params, creatorID) - index++ } if len(statements) == 0 { diff --git a/server/core_wallet.go b/server/core_wallet.go index a6d56e00915b51550af4a0333eb41380ed9e26f2..1a7a8bdcaf46f192edb143682615643dbbb30de7 100644 --- a/server/core_wallet.go +++ b/server/core_wallet.go @@ -44,6 +44,30 @@ type walletLedger struct { UpdateTime int64 } +func (w *walletLedger) GetID() string { + return w.ID +} + +func (w *walletLedger) GetUserID() string { + return w.UserID +} + +func (w *walletLedger) GetCreateTime() int64 { + return w.CreateTime +} + +func (w *walletLedger) GetUpdateTime() int64 { + return w.UpdateTime +} + +func (w *walletLedger) GetChangeset() map[string]interface{} { + return w.Changeset +} + +func (w *walletLedger) GetMetadata() map[string]interface{} { + return w.Metadata +} + func UpdateWallets(logger *zap.Logger, db *sql.DB, updates []*walletUpdate) error { if len(updates) == 0 { return nil diff --git a/server/match_handler.go b/server/match_handler.go index add52e662d8ff2c3c6faf7890dd9b1c4285172ce..8d4a73a988a053a363887f05c022be1cbac0732f 100644 --- a/server/match_handler.go +++ b/server/match_handler.go @@ -15,16 +15,11 @@ package server import ( - "database/sql" "fmt" - "sync" "time" "github.com/gofrs/uuid" - "github.com/heroiclabs/nakama/rtapi" - "github.com/heroiclabs/nakama/social" "github.com/pkg/errors" - "github.com/yuin/gopher-lua" "go.uber.org/atomic" "go.uber.org/zap" ) @@ -39,12 +34,45 @@ type MatchDataMessage struct { ReceiveTime int64 } +func (m *MatchDataMessage) GetUserId() string { + return m.UserID.String() +} +func (m *MatchDataMessage) GetSessionId() string { + return m.SessionID.String() +} +func (m *MatchDataMessage) GetNodeId() string { + return m.Node +} +func (m *MatchDataMessage) GetHidden() bool { + return false +} +func (m *MatchDataMessage) GetPersistence() bool { + return false +} +func (m *MatchDataMessage) GetUsername() string { + return m.Username +} +func (m *MatchDataMessage) GetStatus() string { + return "" +} +func (m *MatchDataMessage) GetOpCode() int64 { + return m.OpCode +} +func (m *MatchDataMessage) GetData() []byte { + return m.Data +} +func (m *MatchDataMessage) GetReceiveTime() int64 { + return m.ReceiveTime +} + type MatchHandler struct { logger *zap.Logger matchRegistry MatchRegistry tracker Tracker router MessageRouter + core RuntimeMatchCore + // Identification not (directly) controlled by match init. ID uuid.UUID Node string @@ -52,15 +80,7 @@ type MatchHandler struct { Stream PresenceStream // Internal state. - tick lua.LNumber - vm *lua.LState - initFn lua.LValue - joinAttemptFn lua.LValue - joinFn lua.LValue - leaveFn lua.LValue - loopFn lua.LValue - ctx *lua.LTable - dispatcher *lua.LTable + tick int64 // Control elements. inputCh chan *MatchDataMessage @@ -74,133 +94,25 @@ type MatchHandler struct { Rate int // Match state. - state lua.LValue + state interface{} } -func NewMatchHandler(logger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, leaderboardCache LeaderboardCache, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter, stdLibs map[string]lua.LGFunction, once *sync.Once, id uuid.UUID, node string, name string, params interface{}) (*MatchHandler, error) { - // Set up the Lua VM that will handle this match. - vm := lua.NewState(lua.Options{ - CallStackSize: config.GetRuntime().CallStackSize, - RegistrySize: config.GetRuntime().RegistrySize, - SkipOpenLibs: true, - IncludeGoStackTrace: true, - }) - for name, lib := range stdLibs { - vm.Push(vm.NewFunction(lib)) - vm.Push(lua.LString(name)) - vm.Call(1, 0) - } - nakamaModule := NewNakamaModule(logger, db, config, socialClient, leaderboardCache, vm, sessionRegistry, matchRegistry, tracker, router, once, nil) - vm.PreloadModule("nakama", nakamaModule.Loader) - - // Create the context to be used throughout this match. - ctx := vm.CreateTable(0, 6) - ctx.RawSetString(__CTX_ENV, ConvertMap(vm, config.GetRuntime().Environment)) - ctx.RawSetString(__CTX_MODE, lua.LString(ExecutionModeMatch.String())) - ctx.RawSetString(__CTX_MATCH_ID, lua.LString(fmt.Sprintf("%v.%v", id.String(), node))) - ctx.RawSetString(__CTX_MATCH_NODE, lua.LString(node)) - - // Require the match module to load it (and its dependencies) and get its returned value. - req := vm.GetGlobal("require").(*lua.LFunction) - err := vm.GPCall(req.GFunction, lua.LString(name)) +func NewMatchHandler(logger *zap.Logger, config Config, matchRegistry MatchRegistry, core RuntimeMatchCore, label *atomic.String, id uuid.UUID, node string, params map[string]interface{}) (*MatchHandler, error) { + state, rateInt, labelStr, err := core.MatchInit(params) if err != nil { - return nil, fmt.Errorf("error loading match module: %v", err.Error()) - } - - // Extract the expected function references. - var tab *lua.LTable - if t := vm.Get(-1); t.Type() != lua.LTTable { - return nil, errors.New("match module must return a table containing the match callback functions") - } else { - tab = t.(*lua.LTable) + return nil, err } - initFn := tab.RawGet(lua.LString("match_init")) - if initFn.Type() != lua.LTFunction { - return nil, errors.New("match_init not found or not a function") - } - joinAttemptFn := tab.RawGet(lua.LString("match_join_attempt")) - if joinAttemptFn.Type() != lua.LTFunction { - return nil, errors.New("match_join_attempt not found or not a function") - } - joinFn := tab.RawGet(lua.LString("match_join")) - if joinFn == nil || joinFn.Type() != lua.LTFunction { - joinFn = nil - } - leaveFn := tab.RawGet(lua.LString("match_leave")) - if leaveFn.Type() != lua.LTFunction { - return nil, errors.New("match_leave not found or not a function") - } - loopFn := tab.RawGet(lua.LString("match_loop")) - if loopFn.Type() != lua.LTFunction { - return nil, errors.New("match_loop not found or not a function") + if state == nil { + return nil, errors.New("Match initial state must not be nil") } - - // Run the match_init sequence. - vm.Push(LSentinel) - vm.Push(initFn) - vm.Push(ctx) - if params == nil { - vm.Push(lua.LNil) - } else { - vm.Push(ConvertValue(vm, params)) - } - - err = vm.PCall(2, lua.MultRet, nil) - if err != nil { - return nil, fmt.Errorf("error running match_init: %v", err.Error()) - } - - // Extract desired label. - label := vm.Get(-1) - if label.Type() == LTSentinel { - return nil, errors.New("match_init returned unexpected third value, must be a label string") - } else if label.Type() != lua.LTString { - return nil, errors.New("match_init returned unexpected third value, must be a label string") - } - vm.Pop(1) - - labelStr := label.String() - if len(labelStr) > 256 { - return nil, errors.New("match_init returned invalid label, must be 256 bytes or less") - } - - // Extract desired tick rate. - rate := vm.Get(-1) - if rate.Type() == LTSentinel { - return nil, errors.New("match_init returned unexpected second value, must be a tick rate number") - } else if rate.Type() != lua.LTNumber { - return nil, errors.New("match_init returned unexpected second value, must be a tick rate number") - } - vm.Pop(1) - - rateInt := int(rate.(lua.LNumber)) - if rateInt > 30 || rateInt < 1 { - return nil, errors.New("match_init returned invalid tick rate, must be between 1 and 30") - } - - // Extract initial state. - state := vm.Get(-1) - if state.Type() == LTSentinel { - return nil, errors.New("match_init returned unexpected first value, must be a state") - } - vm.Pop(1) - - // Drop the sentinel value from the stack. - if sentinel := vm.Get(-1); sentinel.Type() != LTSentinel { - return nil, errors.New("match_init returned too many arguments, must be: state, tick rate number, label string") - } - vm.Pop(1) - - // Add context values only available after match_init completes. - ctx.RawSetString(__CTX_MATCH_LABEL, label) - ctx.RawSetString(__CTX_MATCH_TICK_RATE, rate) + label.Store(labelStr) // Construct the match. mh := &MatchHandler{ - logger: logger.With(zap.String("mid", id.String())), + logger: logger, matchRegistry: matchRegistry, - tracker: tracker, - router: router, + + core: core, ID: id, Node: node, @@ -211,15 +123,7 @@ func NewMatchHandler(logger *zap.Logger, db *sql.DB, config Config, socialClient Label: node, }, - tick: lua.LNumber(0), - vm: vm, - initFn: initFn, - joinAttemptFn: joinAttemptFn, - joinFn: joinFn, - leaveFn: leaveFn, - loopFn: loopFn, - ctx: ctx, - // Dispatcher below. + tick: 0, inputCh: make(chan *MatchDataMessage, config.GetMatch().InputQueueSize), // Ticker below. @@ -227,19 +131,12 @@ func NewMatchHandler(logger *zap.Logger, db *sql.DB, config Config, socialClient stopCh: make(chan struct{}), stopped: atomic.NewBool(false), - Label: atomic.NewString(labelStr), + Label: label, Rate: rateInt, state: state, } - // Set up the dispatcher that exposes control functions to the match loop. - mh.dispatcher = vm.SetFuncs(vm.CreateTable(0, 3), map[string]lua.LGFunction{ - "broadcast_message": mh.broadcastMessage, - "match_kick": mh.matchKick, - "match_label_update": mh.matchLabelUpdate, - }) - // Set up the ticker that governs the match loop. mh.ticker = time.NewTicker(time.Second / time.Duration(mh.Rate)) @@ -310,62 +207,17 @@ func loop(mh *MatchHandler) { return } - // Drain the input queue into a Lua table. - size := len(mh.inputCh) - input := mh.vm.CreateTable(size, 0) - for i := 1; i <= size; i++ { - msg := <-mh.inputCh - - presence := mh.vm.CreateTable(0, 4) - presence.RawSetString("user_id", lua.LString(msg.UserID.String())) - presence.RawSetString("session_id", lua.LString(msg.SessionID.String())) - presence.RawSetString("username", lua.LString(msg.Username)) - presence.RawSetString("node", lua.LString(msg.Node)) - - in := mh.vm.CreateTable(0, 4) - in.RawSetString("sender", presence) - in.RawSetString("op_code", lua.LNumber(msg.OpCode)) - if msg.Data != nil { - in.RawSetString("data", lua.LString(msg.Data)) - } else { - in.RawSetString("data", lua.LNil) - } - in.RawSetString("receive_time_ms", lua.LNumber(msg.ReceiveTime)) - - input.RawSetInt(i, in) - } - - // Execute the match_loop call. - mh.vm.Push(LSentinel) - mh.vm.Push(mh.loopFn) - mh.vm.Push(mh.ctx) - mh.vm.Push(mh.dispatcher) - mh.vm.Push(mh.tick) - mh.vm.Push(mh.state) - mh.vm.Push(input) - - err := mh.vm.PCall(5, lua.MultRet, nil) + state, err := mh.core.MatchLoop(mh.tick, mh.state, mh.inputCh) if err != nil { mh.Stop() - mh.logger.Warn("Stopping match after error from match_loop execution", zap.Int("tick", int(mh.tick)), zap.Error(err)) - return - } - - // Extract the resulting state. - state := mh.vm.Get(-1) - if state.Type() == lua.LTNil || state.Type() == LTSentinel { - mh.logger.Info("Match loop returned nil or no state, stopping match") - mh.Stop() + mh.logger.Warn("Stopping match after error from match_loop execution", zap.Int64("tick", mh.tick), zap.Error(err)) return } - mh.vm.Pop(1) - // Check for and remove the sentinel value, will fail if there are any extra return values. - if sentinel := mh.vm.Get(-1); sentinel.Type() != LTSentinel { - mh.logger.Warn("Match loop returned too many values, stopping match") + if state == nil { mh.Stop() + mh.logger.Info("Match loop returned nil or no state, stopping match") return } - mh.vm.Pop(1) mh.state = state mh.tick++ @@ -378,90 +230,19 @@ func JoinAttempt(resultCh chan *MatchJoinResult, userID, sessionID uuid.UUID, us return } - presence := mh.vm.CreateTable(0, 4) - presence.RawSetString("user_id", lua.LString(userID.String())) - presence.RawSetString("session_id", lua.LString(sessionID.String())) - presence.RawSetString("username", lua.LString(username)) - presence.RawSetString("node", lua.LString(node)) - - // Execute the match_join_attempt call. - mh.vm.Push(LSentinel) - mh.vm.Push(mh.joinAttemptFn) - mh.vm.Push(mh.ctx) - mh.vm.Push(mh.dispatcher) - mh.vm.Push(mh.tick) - mh.vm.Push(mh.state) - mh.vm.Push(presence) - - err := mh.vm.PCall(5, lua.MultRet, nil) + state, allow, reason, err := mh.core.MatchJoinAttempt(mh.tick, mh.state, userID, sessionID, username, node) if err != nil { mh.Stop() - mh.logger.Warn("Stopping match after error from match_join_attempt execution", zap.Int("tick", int(mh.tick)), zap.Error(err)) + mh.logger.Warn("Stopping match after error from match_join_attempt execution", zap.Int64("tick", mh.tick), zap.Error(err)) resultCh <- &MatchJoinResult{Allow: false} return } - - allowFound := false - var allow bool - var reason string - - // Extract the join attempt response. - allowOrReason := mh.vm.Get(-1) - if allowOrReason.Type() == LTSentinel { - mh.logger.Warn("Match join attempt returned too few values, stopping match - expected: state, join result boolean, optional reject reason string") - mh.Stop() - resultCh <- &MatchJoinResult{Allow: false} - return - } else if allowOrReason.Type() == lua.LTString { - // This was the optional reject reason string. - reason = allowOrReason.String() - } else if allowOrReason.Type() == lua.LTBool { - // This was the required join result boolean, expect no reason as it was skipped. - allowFound = true - allow = lua.LVAsBool(allowOrReason) - } else { - mh.logger.Warn("Match join attempt returned non-boolean join result or non-string reject reason, stopping match") + if state == nil { mh.Stop() - resultCh <- &MatchJoinResult{Allow: false} - return - } - mh.vm.Pop(1) - - if !allowFound { - // The previous parameter was the optional reject reason string, now look for the required join result boolean. - allowRequired := mh.vm.Get(-1) - if allowRequired.Type() == LTSentinel { - mh.logger.Warn("Match join attempt returned incorrect or too few values, stopping match - expected: state, join result boolean, optional reject reason string") - mh.Stop() - resultCh <- &MatchJoinResult{Allow: false} - return - } else if allowRequired.Type() != lua.LTBool { - mh.logger.Warn("Match join attempt returned non-boolean join result, stopping match") - mh.Stop() - resultCh <- &MatchJoinResult{Allow: false} - return - } - allow = lua.LVAsBool(allowRequired) - mh.vm.Pop(1) - } - - // Extract the resulting state. - state := mh.vm.Get(-1) - if state.Type() == lua.LTNil || state.Type() == LTSentinel { mh.logger.Info("Match join attempt returned nil or no state, stopping match") - mh.Stop() resultCh <- &MatchJoinResult{Allow: false} return } - mh.vm.Pop(1) - // Check for and remove the sentinel value, will fail if there are any extra return values. - if sentinel := mh.vm.Get(-1); sentinel.Type() != LTSentinel { - mh.logger.Warn("Match join attempt returned too many values, stopping match") - mh.Stop() - resultCh <- &MatchJoinResult{Allow: false} - return - } - mh.vm.Pop(1) mh.state = state resultCh <- &MatchJoinResult{Allow: allow, Reason: reason, Label: mh.Label.Load()} @@ -470,56 +251,21 @@ func JoinAttempt(resultCh chan *MatchJoinResult, userID, sessionID uuid.UUID, us func Join(joins []*MatchPresence) func(mh *MatchHandler) { return func(mh *MatchHandler) { - if mh.joinFn == nil { - return - } - if mh.stopped.Load() { return } - presences := mh.vm.CreateTable(len(joins), 0) - for i, p := range joins { - presence := mh.vm.CreateTable(0, 4) - presence.RawSetString("user_id", lua.LString(p.UserID.String())) - presence.RawSetString("session_id", lua.LString(p.SessionID.String())) - presence.RawSetString("username", lua.LString(p.Username)) - presence.RawSetString("node", lua.LString(p.Node)) - - presences.RawSetInt(i+1, presence) - } - - // Execute the match_leave call. - mh.vm.Push(LSentinel) - mh.vm.Push(mh.joinFn) - mh.vm.Push(mh.ctx) - mh.vm.Push(mh.dispatcher) - mh.vm.Push(mh.tick) - mh.vm.Push(mh.state) - mh.vm.Push(presences) - - err := mh.vm.PCall(5, lua.MultRet, nil) + state, err := mh.core.MatchJoin(mh.tick, mh.state, joins) if err != nil { mh.Stop() - mh.logger.Warn("Stopping match after error from match_join execution", zap.Int("tick", int(mh.tick)), zap.Error(err)) + mh.logger.Warn("Stopping match after error from match_join execution", zap.Int64("tick", mh.tick), zap.Error(err)) return } - - // Extract the resulting state. - state := mh.vm.Get(-1) - if state.Type() == lua.LTNil || state.Type() == LTSentinel { - mh.logger.Info("Match join returned nil or no state, stopping match") - mh.Stop() - return - } - mh.vm.Pop(1) - // Check for and remove the sentinel value, will fail if there are any extra return values. - if sentinel := mh.vm.Get(-1); sentinel.Type() != LTSentinel { - mh.logger.Warn("Match join returned too many values, stopping match") + if state == nil { mh.Stop() + mh.logger.Info("Match join returned nil or no state, stopping match") return } - mh.vm.Pop(1) mh.state = state } @@ -531,280 +277,18 @@ func Leave(leaves []*MatchPresence) func(mh *MatchHandler) { return } - presences := mh.vm.CreateTable(len(leaves), 0) - for i, p := range leaves { - presence := mh.vm.CreateTable(0, 4) - presence.RawSetString("user_id", lua.LString(p.UserID.String())) - presence.RawSetString("session_id", lua.LString(p.SessionID.String())) - presence.RawSetString("username", lua.LString(p.Username)) - presence.RawSetString("node", lua.LString(p.Node)) - - presences.RawSetInt(i+1, presence) - } - - // Execute the match_leave call. - mh.vm.Push(LSentinel) - mh.vm.Push(mh.leaveFn) - mh.vm.Push(mh.ctx) - mh.vm.Push(mh.dispatcher) - mh.vm.Push(mh.tick) - mh.vm.Push(mh.state) - mh.vm.Push(presences) - - err := mh.vm.PCall(5, lua.MultRet, nil) + state, err := mh.core.MatchLeave(mh.tick, mh.state, leaves) if err != nil { mh.Stop() mh.logger.Warn("Stopping match after error from match_leave execution", zap.Int("tick", int(mh.tick)), zap.Error(err)) return } - - // Extract the resulting state. - state := mh.vm.Get(-1) - if state.Type() == lua.LTNil || state.Type() == LTSentinel { - mh.logger.Info("Match leave returned nil or no state, stopping match") - mh.Stop() - return - } - mh.vm.Pop(1) - // Check for and remove the sentinel value, will fail if there are any extra return values. - if sentinel := mh.vm.Get(-1); sentinel.Type() != LTSentinel { - mh.logger.Warn("Match leave returned too many values, stopping match") + if state == nil { mh.Stop() + mh.logger.Info("Match leave returned nil or no state, stopping match") return } - mh.vm.Pop(1) mh.state = state } } - -func (mh *MatchHandler) broadcastMessage(l *lua.LState) int { - opCode := l.CheckInt64(1) - - var dataBytes []byte - if data := l.Get(2); data.Type() != lua.LTNil { - if data.Type() != lua.LTString { - l.ArgError(2, "expects data to be a string or nil") - return 0 - } - dataBytes = []byte(data.(lua.LString)) - } - - filter := l.OptTable(3, nil) - var presenceIDs []*PresenceID - if filter != nil { - fl := filter.Len() - if fl == 0 { - return 0 - } - presenceIDs = make([]*PresenceID, 0, fl) - conversionError := false - filter.ForEach(func(_, p lua.LValue) { - pt, ok := p.(*lua.LTable) - if !ok { - conversionError = true - l.ArgError(1, "expects a valid set of presences") - return - } - - presenceID := &PresenceID{} - pt.ForEach(func(k, v lua.LValue) { - switch k.String() { - case "session_id": - sid, err := uuid.FromString(v.String()) - if err != nil { - conversionError = true - l.ArgError(1, "expects each presence to have a valid session_id") - return - } - presenceID.SessionID = sid - case "node": - if v.Type() != lua.LTString { - conversionError = true - l.ArgError(1, "expects node to be string") - return - } - presenceID.Node = v.String() - } - }) - if presenceID.SessionID == uuid.Nil || presenceID.Node == "" { - conversionError = true - l.ArgError(1, "expects each presence to have a valid session_id and node") - return - } - if conversionError { - return - } - presenceIDs = append(presenceIDs, presenceID) - }) - if conversionError { - return 0 - } - } - - if presenceIDs != nil && len(presenceIDs) == 0 { - // Filter is empty, there are no requested message targets. - return 0 - } - - sender := l.OptTable(4, nil) - var presence *rtapi.UserPresence - if sender != nil { - presence = &rtapi.UserPresence{} - conversionError := false - sender.ForEach(func(k, v lua.LValue) { - switch k.String() { - case "user_id": - s := v.String() - _, err := uuid.FromString(s) - if err != nil { - conversionError = true - l.ArgError(4, "expects presence to have a valid user_id") - return - } - presence.UserId = s - case "session_id": - s := v.String() - _, err := uuid.FromString(s) - if err != nil { - conversionError = true - l.ArgError(4, "expects presence to have a valid session_id") - return - } - presence.SessionId = s - case "username": - if v.Type() != lua.LTString { - conversionError = true - l.ArgError(4, "expects username to be string") - return - } - presence.Username = v.String() - } - }) - if presence.UserId == "" || presence.SessionId == "" || presence.Username == "" { - l.ArgError(4, "expects presence to have a valid user_id, session_id, and username") - return 0 - } - if conversionError { - return 0 - } - } - - if presenceIDs != nil { - // Ensure specific presences actually exist to prevent sending bogus messages to arbitrary users. - actualPresenceIDs := mh.tracker.ListPresenceIDByStream(mh.Stream) - for i := 0; i < len(presenceIDs); i++ { - found := false - presenceID := presenceIDs[i] - for j := 0; j < len(actualPresenceIDs); j++ { - if actual := actualPresenceIDs[j]; presenceID.SessionID == actual.SessionID && presenceID.Node == actual.Node { - // If it matches, drop it. - actualPresenceIDs[j] = actualPresenceIDs[len(actualPresenceIDs)-1] - actualPresenceIDs = actualPresenceIDs[:len(actualPresenceIDs)-1] - found = true - break - } - } - if !found { - // If this presence wasn't in the filters, it's not needed. - presenceIDs[i] = presenceIDs[len(presenceIDs)-1] - presenceIDs = presenceIDs[:len(presenceIDs)-1] - i-- - } - } - if len(presenceIDs) == 0 { - // None of the target presenceIDs existed in the list of match members. - return 0 - } - } - - msg := &rtapi.Envelope{Message: &rtapi.Envelope_MatchData{MatchData: &rtapi.MatchData{ - MatchId: mh.IDStr, - Presence: presence, - OpCode: opCode, - Data: dataBytes, - }}} - - if presenceIDs == nil { - mh.router.SendToStream(mh.logger, mh.Stream, msg) - } else { - mh.router.SendToPresenceIDs(mh.logger, presenceIDs, true, StreamModeMatchAuthoritative, msg) - } - - return 0 -} - -func (mh *MatchHandler) matchKick(l *lua.LState) int { - input := l.OptTable(1, nil) - if input == nil { - return 0 - } - size := input.Len() - if size == 0 { - return 0 - } - - presences := make([]*MatchPresence, 0, size) - conversionError := false - input.ForEach(func(_, p lua.LValue) { - pt, ok := p.(*lua.LTable) - if !ok { - conversionError = true - l.ArgError(1, "expects a valid set of presences") - return - } - - presence := &MatchPresence{} - pt.ForEach(func(k, v lua.LValue) { - switch k.String() { - case "user_id": - uid, err := uuid.FromString(v.String()) - if err != nil { - conversionError = true - l.ArgError(1, "expects each presence to have a valid user_id") - return - } - presence.UserID = uid - case "session_id": - sid, err := uuid.FromString(v.String()) - if err != nil { - conversionError = true - l.ArgError(1, "expects each presence to have a valid session_id") - return - } - presence.SessionID = sid - case "node": - if v.Type() != lua.LTString { - conversionError = true - l.ArgError(1, "expects node to be string") - return - } - presence.Node = v.String() - } - }) - if presence.UserID == uuid.Nil || presence.SessionID == uuid.Nil || presence.Node == "" { - conversionError = true - l.ArgError(1, "expects each presence to have a valid user_id, session_id, and node") - return - } - if conversionError { - return - } - presences = append(presences, presence) - }) - if conversionError { - return 0 - } - - mh.matchRegistry.Kick(mh.Stream, presences) - return 0 -} - -func (mh *MatchHandler) matchLabelUpdate(l *lua.LState) int { - input := l.OptString(1, "") - - mh.Label.Store(input) - // This must be executed from inside a match call so safe to update here. - mh.ctx.RawSetString(__CTX_MATCH_LABEL, lua.LString(input)) - return 0 -} diff --git a/server/match_registry.go b/server/match_registry.go index aa5355756e904057a638ef1ff4ee4afe01b373d2..ac8b39c6fba2e7b165412b6019d543b85af06183 100644 --- a/server/match_registry.go +++ b/server/match_registry.go @@ -15,13 +15,11 @@ package server import ( - "database/sql" "fmt" "github.com/gofrs/uuid" "github.com/golang/protobuf/ptypes/wrappers" "github.com/heroiclabs/nakama/api" - "github.com/heroiclabs/nakama/social" - "github.com/yuin/gopher-lua" + "go.uber.org/atomic" "go.uber.org/zap" "sync" "time" @@ -43,6 +41,28 @@ type MatchPresence struct { Username string } +func (p *MatchPresence) GetUserId() string { + return p.UserID.String() +} +func (p *MatchPresence) GetSessionId() string { + return p.SessionID.String() +} +func (p *MatchPresence) GetNodeId() string { + return p.Node +} +func (p *MatchPresence) GetHidden() bool { + return false +} +func (p *MatchPresence) GetPersistence() bool { + return false +} +func (p *MatchPresence) GetUsername() string { + return p.Username +} +func (p *MatchPresence) GetStatus() string { + return "" +} + type MatchJoinResult struct { Allow bool Reason string @@ -51,7 +71,7 @@ type MatchJoinResult struct { type MatchRegistry interface { // Create and start a new match, given a Lua module name. - NewMatch(name string, params interface{}) (*MatchHandler, error) + NewMatch(logger *zap.Logger, id uuid.UUID, label *atomic.String, core RuntimeMatchCore, params map[string]interface{}) (*MatchHandler, error) // Return a match handler by ID, only from the local node. GetMatch(id uuid.UUID) *MatchHandler // Remove a tracked match and ensure all its presences are cleaned up. @@ -80,44 +100,29 @@ type MatchRegistry interface { type LocalMatchRegistry struct { sync.RWMutex - logger *zap.Logger - db *sql.DB - config Config - socialClient *social.Client - leaderboardCache LeaderboardCache - sessionRegistry *SessionRegistry - tracker Tracker - router MessageRouter - stdLibs map[string]lua.LGFunction - modules *sync.Map - once *sync.Once - node string - matches map[uuid.UUID]*MatchHandler + logger *zap.Logger + config Config + tracker Tracker + node string + matches map[uuid.UUID]*MatchHandler } -func NewLocalMatchRegistry(logger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, leaderboardCache LeaderboardCache, sessionRegistry *SessionRegistry, tracker Tracker, router MessageRouter, stdLibs map[string]lua.LGFunction, once *sync.Once, node string) MatchRegistry { +func NewLocalMatchRegistry(logger *zap.Logger, config Config, tracker Tracker, node string) MatchRegistry { return &LocalMatchRegistry{ - logger: logger, - db: db, - config: config, - socialClient: socialClient, - leaderboardCache: leaderboardCache, - sessionRegistry: sessionRegistry, - tracker: tracker, - router: router, - stdLibs: stdLibs, - once: once, - node: node, - matches: make(map[uuid.UUID]*MatchHandler), + logger: logger, + config: config, + tracker: tracker, + node: node, + matches: make(map[uuid.UUID]*MatchHandler), } } -func (r *LocalMatchRegistry) NewMatch(name string, params interface{}) (*MatchHandler, error) { - id := uuid.Must(uuid.NewV4()) - match, err := NewMatchHandler(r.logger, r.db, r.config, r.socialClient, r.leaderboardCache, r.sessionRegistry, r, r.tracker, r.router, r.stdLibs, r.once, id, r.node, name, params) +func (r *LocalMatchRegistry) NewMatch(logger *zap.Logger, id uuid.UUID, label *atomic.String, core RuntimeMatchCore, params map[string]interface{}) (*MatchHandler, error) { + match, err := NewMatchHandler(logger, r.config, r, core, label, id, r.node, params) if err != nil { return nil, err } + r.Lock() r.matches[id] = match r.Unlock() diff --git a/server/matchmaker.go b/server/matchmaker.go index a9aaaecde606d3d99ff1999d12af83b9eddc2611..83994ff00623923096193fa8a5acd00fed9756f2 100644 --- a/server/matchmaker.go +++ b/server/matchmaker.go @@ -19,6 +19,7 @@ import ( "github.com/blevesearch/bleve" "github.com/gofrs/uuid" + "github.com/heroiclabs/nakama/runtime" "github.com/pkg/errors" "go.uber.org/zap" ) @@ -32,6 +33,28 @@ type MatchmakerPresence struct { Node string `json:"node"` } +func (p *MatchmakerPresence) GetUserId() string { + return p.UserId +} +func (p *MatchmakerPresence) GetSessionId() string { + return p.SessionId +} +func (p *MatchmakerPresence) GetNodeId() string { + return p.Node +} +func (p *MatchmakerPresence) GetHidden() bool { + return false +} +func (p *MatchmakerPresence) GetPersistence() bool { + return false +} +func (p *MatchmakerPresence) GetUsername() string { + return p.Username +} +func (p *MatchmakerPresence) GetStatus() string { + return "" +} + type MatchmakerEntry struct { Ticket string `json:"ticket"` Presence *MatchmakerPresence `json:"presence"` @@ -42,6 +65,16 @@ type MatchmakerEntry struct { SessionID uuid.UUID `json:"-"` } +func (m *MatchmakerEntry) GetPresence() runtime.Presence { + return m.Presence +} +func (m *MatchmakerEntry) GetTicket() string { + return m.Ticket +} +func (m *MatchmakerEntry) GetProperties() map[string]interface{} { + return m.Properties +} + type Matchmaker interface { Add(session Session, query string, minCount int, maxCount int, stringProperties map[string]string, numericProperties map[string]float64) (string, []*MatchmakerEntry, error) Remove(sessionID uuid.UUID, ticket string) error diff --git a/server/pipeline.go b/server/pipeline.go index c6c33af61648a06ab488f464b7d6092d10b8ef9c..37c7efd16c530fc5ff0b096b7e888020f10ab5bd 100644 --- a/server/pipeline.go +++ b/server/pipeline.go @@ -31,6 +31,7 @@ import ( ) type Pipeline struct { + logger *zap.Logger config Config db *sql.DB jsonpbMarshaler *jsonpb.Marshaler @@ -40,12 +41,13 @@ type Pipeline struct { matchmaker Matchmaker tracker Tracker router MessageRouter - runtimePool *RuntimePool + runtime *Runtime node string } -func NewPipeline(config Config, db *sql.DB, jsonpbMarshaler *jsonpb.Marshaler, jsonpbUnmarshaler *jsonpb.Unmarshaler, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, matchmaker Matchmaker, tracker Tracker, router MessageRouter, runtimePool *RuntimePool) *Pipeline { +func NewPipeline(logger *zap.Logger, config Config, db *sql.DB, jsonpbMarshaler *jsonpb.Marshaler, jsonpbUnmarshaler *jsonpb.Unmarshaler, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, matchmaker Matchmaker, tracker Tracker, router MessageRouter, runtime *Runtime) *Pipeline { return &Pipeline{ + logger: logger, config: config, db: db, jsonpbMarshaler: jsonpbMarshaler, @@ -55,7 +57,7 @@ func NewPipeline(config Config, db *sql.DB, jsonpbMarshaler *jsonpb.Marshaler, j matchmaker: matchmaker, tracker: tracker, router: router, - runtimePool: runtimePool, + runtime: runtime, node: config.GetName(), } } @@ -133,53 +135,47 @@ func (p *Pipeline) ProcessRequest(logger *zap.Logger, session Session, envelope return false } - var messageName string + var messageName, messageNameID string switch envelope.Message.(type) { case *rtapi.Envelope_Rpc: // No before/after hooks on RPC. default: messageName = fmt.Sprintf("%T", envelope.Message) - - // Stats measurement start boundary. - name := fmt.Sprintf("nakama.rtapi-before.%v", pipelineName) - statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) - startNanos := time.Now().UTC().UnixNano() - span := trace.NewSpan(name, nil, trace.StartOptions{}) - - // Actual before hook function execution. - hookResult, hookErr := invokeReqBeforeHook(logger, p.config, p.runtimePool, p.jsonpbMarshaler, p.jsonpbUnmarshaler, session.ID().String(), session.UserID(), session.Username(), session.Expiry(), session.ClientIP(), session.ClientPort(), messageName, envelope) - - // Stats measurement end boundary. - span.End() - stats.Record(statsCtx, MetricsRtapiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsRtapiCount.M(1)) - - if hookErr != nil { - session.Send(false, 0, &rtapi.Envelope{Cid: envelope.Cid, Message: &rtapi.Envelope_Error{Error: &rtapi.Error{ - Code: int32(rtapi.Error_RUNTIME_FUNCTION_EXCEPTION), - Message: hookErr.Error(), - }}}) - return false - } else if hookResult == nil { - // if result is nil, requested resource is disabled. - logger.Warn("Intercepted a disabled resource.", zap.String("resource", messageName)) - session.Send(false, 0, &rtapi.Envelope{Cid: envelope.Cid, Message: &rtapi.Envelope_Error{Error: &rtapi.Error{ - Code: int32(rtapi.Error_UNRECOGNIZED_PAYLOAD), - Message: "Requested resource was not found.", - }}}) - return false - } - - resultCast, ok := hookResult.(*rtapi.Envelope) - if !ok { - logger.Error("Invalid runtime Before function result. Make sure that the result matches the structure of the payload.", zap.Any("payload", envelope), zap.Any("result", hookResult)) - session.Send(false, 0, &rtapi.Envelope{Cid: envelope.Cid, Message: &rtapi.Envelope_Error{Error: &rtapi.Error{ - Code: int32(rtapi.Error_RUNTIME_FUNCTION_EXCEPTION), - Message: "Invalid runtime Before function result.", - }}}) - return false + messageNameID = strings.ToLower(messageName) + + if fn := p.runtime.BeforeRt(messageNameID); fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("nakama.rtapi.%v-before", pipelineName) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Actual before hook function execution. + hookResult, hookErr := fn(logger, session.UserID().String(), session.Username(), session.Expiry(), session.ID().String(), session.ClientIP(), session.ClientPort(), envelope) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsRtapiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsRtapiCount.M(1)) + + if hookErr != nil { + session.Send(false, 0, &rtapi.Envelope{Cid: envelope.Cid, Message: &rtapi.Envelope_Error{Error: &rtapi.Error{ + Code: int32(rtapi.Error_RUNTIME_FUNCTION_EXCEPTION), + Message: hookErr.Error(), + }}}) + return false + } else if hookResult == nil { + // if result is nil, requested resource is disabled. + logger.Warn("Intercepted a disabled resource.", zap.String("resource", messageName)) + session.Send(false, 0, &rtapi.Envelope{Cid: envelope.Cid, Message: &rtapi.Envelope_Error{Error: &rtapi.Error{ + Code: int32(rtapi.Error_UNRECOGNIZED_PAYLOAD), + Message: "Requested resource was not found.", + }}}) + return false + } + + envelope = hookResult } - envelope = resultCast } // Stats measurement start boundary. @@ -196,18 +192,20 @@ func (p *Pipeline) ProcessRequest(logger *zap.Logger, session Session, envelope stats.Record(statsCtx, MetricsRtapiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsRtapiCount.M(1)) if messageName != "" { - // Stats measurement start boundary. - name := fmt.Sprintf("nakama.rtapi-after.%v", pipelineName) - statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) - startNanos := time.Now().UTC().UnixNano() - span := trace.NewSpan(name, nil, trace.StartOptions{}) - - // Actual after hook function execution. - invokeReqAfterHook(logger, p.config, p.runtimePool, p.jsonpbMarshaler, session.ID().String(), session.UserID(), session.Username(), session.Expiry(), session.ClientIP(), session.ClientPort(), messageName, envelope) - - // Stats measurement end boundary. - span.End() - stats.Record(statsCtx, MetricsRtapiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsRtapiCount.M(1)) + if fn := p.runtime.AfterRt(messageNameID); fn != nil { + // Stats measurement start boundary. + name := fmt.Sprintf("nakama.rtapi.%v-after", pipelineName) + statsCtx, _ := tag.New(context.Background(), tag.Upsert(MetricsFunction, name)) + startNanos := time.Now().UTC().UnixNano() + span := trace.NewSpan(name, nil, trace.StartOptions{}) + + // Actual after hook function execution. + fn(logger, session.UserID().String(), session.Username(), session.Expiry(), session.ID().String(), session.ClientIP(), session.ClientPort(), envelope) + + // Stats measurement end boundary. + span.End() + stats.Record(statsCtx, MetricsRtapiTimeSpentMsec.M(float64(time.Now().UTC().UnixNano()-startNanos)/1000), MetricsRtapiCount.M(1)) + } } return true diff --git a/server/pipeline_matchmaker.go b/server/pipeline_matchmaker.go index abfdad9bd375ea4eaf9e15ba25ac856959438828..54c6a5be1e226f5af6551ca9870131834490705d 100644 --- a/server/pipeline_matchmaker.go +++ b/server/pipeline_matchmaker.go @@ -67,8 +67,18 @@ func (p *Pipeline) matchmakerAdd(logger *zap.Logger, session Session, envelope * return } + var tokenOrMatchID string + var isMatchID bool + // Check if there's a matchmaker matched runtime callback, call it, and see if it returns a match ID. - tokenOrMatchID, isMatchID := invokeMatchmakerMatchedHook(logger, p.runtimePool, entries) + fn := p.runtime.MatchmakerMatched() + if fn != nil { + tokenOrMatchID, isMatchID, err = fn(entries) + if err != nil { + p.logger.Error("Error running Matchmaker Matched hook.", zap.Error(err)) + } + } + if !isMatchID { // If there was no callback or it didn't return a valid match ID always return at least a token. token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ diff --git a/server/pipeline_rpc.go b/server/pipeline_rpc.go index cbcfca0f4f0da72a5cfbd55d57006d59843c7791..e06974d0cf4152bf099eefbc0776e82b9ba3a14e 100644 --- a/server/pipeline_rpc.go +++ b/server/pipeline_rpc.go @@ -19,9 +19,7 @@ import ( "github.com/heroiclabs/nakama/api" "github.com/heroiclabs/nakama/rtapi" - "github.com/yuin/gopher-lua" "go.uber.org/zap" - "go.uber.org/zap/zapcore" ) func (p *Pipeline) rpc(logger *zap.Logger, session Session, envelope *rtapi.Envelope) { @@ -36,7 +34,8 @@ func (p *Pipeline) rpc(logger *zap.Logger, session Session, envelope *rtapi.Enve id := strings.ToLower(rpcMessage.Id) - if !p.runtimePool.HasCallback(ExecutionModeRPC, id) { + fn := p.runtime.Rpc(id) + if fn == nil { session.Send(false, 0, &rtapi.Envelope{Cid: envelope.Cid, Message: &rtapi.Envelope_Error{Error: &rtapi.Error{ Code: int32(rtapi.Error_RUNTIME_FUNCTION_NOT_FOUND), Message: "RPC function not found", @@ -44,61 +43,17 @@ func (p *Pipeline) rpc(logger *zap.Logger, session Session, envelope *rtapi.Enve return } - runtime := p.runtimePool.Get() - lf := runtime.GetCallback(ExecutionModeRPC, id) - if lf == nil { - p.runtimePool.Put(runtime) - session.Send(false, 0, &rtapi.Envelope{Cid: envelope.Cid, Message: &rtapi.Envelope_Error{Error: &rtapi.Error{ - Code: int32(rtapi.Error_RUNTIME_FUNCTION_NOT_FOUND), - Message: "RPC function not found", - }}}) - return - } - - result, fnErr, _ := runtime.InvokeFunction(ExecutionModeRPC, lf, nil, session.UserID().String(), session.Username(), session.Expiry(), session.ID().String(), session.ClientIP(), session.ClientPort(), rpcMessage.Payload) - p.runtimePool.Put(runtime) + result, fnErr, _ := fn(nil, session.UserID().String(), session.Username(), session.Expiry(), session.ID().String(), session.ClientIP(), session.ClientPort(), rpcMessage.Payload) if fnErr != nil { - logger.Error("Runtime RPC function caused an error", zap.String("id", rpcMessage.Id), zap.Error(fnErr)) - if apiErr, ok := fnErr.(*lua.ApiError); ok && !logger.Core().Enabled(zapcore.InfoLevel) { - msg := apiErr.Object.String() - if strings.HasPrefix(msg, lf.Proto.SourceName) { - msg = msg[len(lf.Proto.SourceName):] - msgParts := strings.SplitN(msg, ": ", 2) - if len(msgParts) == 2 { - msg = msgParts[1] - } else { - msg = msgParts[0] - } - } - session.Send(false, 0, &rtapi.Envelope{Cid: envelope.Cid, Message: &rtapi.Envelope_Error{Error: &rtapi.Error{ - Code: int32(rtapi.Error_RUNTIME_FUNCTION_EXCEPTION), - Message: msg, - }}}) - } else { - session.Send(false, 0, &rtapi.Envelope{Cid: envelope.Cid, Message: &rtapi.Envelope_Error{Error: &rtapi.Error{ - Code: int32(rtapi.Error_RUNTIME_FUNCTION_EXCEPTION), - Message: fnErr.Error(), - }}}) - } - return - } - - if result == nil { - session.Send(false, 0, &rtapi.Envelope{Cid: envelope.Cid, Message: &rtapi.Envelope_Rpc{Rpc: &api.Rpc{ - Id: rpcMessage.Id, - }}}) - return - } - - if payload, ok := result.(string); !ok { session.Send(false, 0, &rtapi.Envelope{Cid: envelope.Cid, Message: &rtapi.Envelope_Error{Error: &rtapi.Error{ Code: int32(rtapi.Error_RUNTIME_FUNCTION_EXCEPTION), - Message: "Runtime function returned invalid data - only allowed one return value of type String/Byte.", - }}}) - } else { - session.Send(false, 0, &rtapi.Envelope{Cid: envelope.Cid, Message: &rtapi.Envelope_Rpc{Rpc: &api.Rpc{ - Id: rpcMessage.Id, - Payload: payload, + Message: fnErr.Error(), }}}) + return } + + session.Send(false, 0, &rtapi.Envelope{Cid: envelope.Cid, Message: &rtapi.Envelope_Rpc{Rpc: &api.Rpc{ + Id: rpcMessage.Id, + Payload: result, + }}}) } diff --git a/server/runtime.go b/server/runtime.go index 9f8c46c40ae1b6ca5b1bb23879163b132ae9460b..c20267e08a495c5c7b8e6ae8681b04291812ac9e 100644 --- a/server/runtime.go +++ b/server/runtime.go @@ -16,343 +16,1545 @@ package server import ( "database/sql" - - "bytes" - "sync" - - "context" - + "github.com/gofrs/uuid" + "github.com/golang/protobuf/jsonpb" + "github.com/golang/protobuf/ptypes/empty" + "github.com/heroiclabs/nakama/api" + "github.com/heroiclabs/nakama/rtapi" "github.com/heroiclabs/nakama/social" - "github.com/yuin/gopher-lua" - "go.opencensus.io/stats" + "github.com/pkg/errors" "go.uber.org/zap" "google.golang.org/grpc/codes" + "os" + "path/filepath" + "strings" ) -const LTSentinel = lua.LValueType(-1) - -type LSentinelType struct { - lua.LNilType -} - -func (s *LSentinelType) String() string { return "" } -func (s *LSentinelType) Type() lua.LValueType { return LTSentinel } - -var LSentinel = lua.LValue(&LSentinelType{}) - -type RuntimeModule struct { - Name string - Path string - Content []byte -} - -type RuntimePool struct { - sync.Mutex - logger *zap.Logger - regCallbacks *RegCallbacks - moduleCache *ModuleCache - poolCh chan *Runtime - maxCount int - currentCount int - newFn func() *Runtime - - statsCtx context.Context -} - -func NewRuntimePool(logger, startupLogger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, leaderboardCache LeaderboardCache, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter, stdLibs map[string]lua.LGFunction, moduleCache *ModuleCache, regCallbacks *RegCallbacks, once *sync.Once) *RuntimePool { - rp := &RuntimePool{ - logger: logger, - regCallbacks: regCallbacks, - moduleCache: moduleCache, - poolCh: make(chan *Runtime, config.GetRuntime().MaxCount), - maxCount: config.GetRuntime().MaxCount, - // Set the current count assuming we'll warm up the pool in a moment. - currentCount: config.GetRuntime().MinCount, - newFn: func() *Runtime { - r, err := newVM(logger, db, config, socialClient, leaderboardCache, sessionRegistry, matchRegistry, tracker, router, stdLibs, moduleCache, once, nil) - if err != nil { - logger.Fatal("Failed initializing runtime.", zap.Error(err)) - } - return r - }, - statsCtx: context.Background(), - } - - // Warm up the pool. - startupLogger.Info("Allocating minimum runtime pool", zap.Int("count", rp.currentCount)) - if len(moduleCache.Names) > 0 { - // Only if there are runtime modules to load. - for i := 0; i < config.GetRuntime().MinCount; i++ { - rp.poolCh <- rp.newFn() - } - stats.Record(rp.statsCtx, MetricsRuntimeCount.M(int64(config.GetRuntime().MinCount))) - } - startupLogger.Info("Allocated minimum runtime pool") +var ( + ErrRuntimeRPCNotFound = errors.New("RPC function not found") +) - return rp -} +const API_PREFIX = "/nakama.api.Nakama/" +const RTAPI_PREFIX = "*rtapi.Envelope_" -func (rp *RuntimePool) HasCallback(mode ExecutionMode, id string) bool { - ok := false - switch mode { - case ExecutionModeRPC: - _, ok = rp.regCallbacks.RPC[id] - case ExecutionModeBefore: - _, ok = rp.regCallbacks.Before[id] - case ExecutionModeAfter: - _, ok = rp.regCallbacks.After[id] - case ExecutionModeMatchmaker: - ok = rp.regCallbacks.Matchmaker != nil - } +type ( + RuntimeRpcFunction func(queryParams map[string][]string, userID, username string, expiry int64, sessionID, clientIP, clientPort, payload string) (string, error, codes.Code) - return ok -} + RuntimeBeforeRtFunction func(logger *zap.Logger, userID, username string, expiry int64, sessionID, clientIP, clientPort string, envelope *rtapi.Envelope) (*rtapi.Envelope, error) + RuntimeAfterRtFunction func(logger *zap.Logger, userID, username string, expiry int64, sessionID, clientIP, clientPort string, envelope *rtapi.Envelope) error -func (rp *RuntimePool) Get() *Runtime { - select { - case r := <-rp.poolCh: - // Ideally use an available idle runtime. - return r - default: - // If there was no idle runtime, see if we can allocate a new one. - rp.Lock() - if rp.currentCount >= rp.maxCount { - rp.Unlock() - // If we've reached the max allowed allocation block on an available runtime. - return <-rp.poolCh - } - // Inside the locked region now, last chance to use an available idle runtime. - // Note: useful in case a runtime becomes available while waiting to acquire lock. - select { - case r := <-rp.poolCh: - rp.Unlock() - return r - default: - // Allocate a new runtime. - rp.currentCount++ - rp.Unlock() - stats.Record(rp.statsCtx, MetricsRuntimeCount.M(1)) - return rp.newFn() - } + RuntimeBeforeGetAccountFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) (*empty.Empty, error, codes.Code) + RuntimeAfterGetAccountFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.Account) error + RuntimeBeforeUpdateAccountFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.UpdateAccountRequest) (*api.UpdateAccountRequest, error, codes.Code) + RuntimeAfterUpdateAccountFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeAuthenticateCustomFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AuthenticateCustomRequest) (*api.AuthenticateCustomRequest, error, codes.Code) + RuntimeAfterAuthenticateCustomFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.Session) error + RuntimeBeforeAuthenticateDeviceFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AuthenticateDeviceRequest) (*api.AuthenticateDeviceRequest, error, codes.Code) + RuntimeAfterAuthenticateDeviceFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.Session) error + RuntimeBeforeAuthenticateEmailFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AuthenticateEmailRequest) (*api.AuthenticateEmailRequest, error, codes.Code) + RuntimeAfterAuthenticateEmailFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.Session) error + RuntimeBeforeAuthenticateFacebookFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AuthenticateFacebookRequest) (*api.AuthenticateFacebookRequest, error, codes.Code) + RuntimeAfterAuthenticateFacebookFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.Session) error + RuntimeBeforeAuthenticateGameCenterFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AuthenticateGameCenterRequest) (*api.AuthenticateGameCenterRequest, error, codes.Code) + RuntimeAfterAuthenticateGameCenterFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.Session) error + RuntimeBeforeAuthenticateGoogleFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AuthenticateGoogleRequest) (*api.AuthenticateGoogleRequest, error, codes.Code) + RuntimeAfterAuthenticateGoogleFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.Session) error + RuntimeBeforeAuthenticateSteamFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AuthenticateSteamRequest) (*api.AuthenticateSteamRequest, error, codes.Code) + RuntimeAfterAuthenticateSteamFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.Session) error + RuntimeBeforeListChannelMessagesFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ListChannelMessagesRequest) (*api.ListChannelMessagesRequest, error, codes.Code) + RuntimeAfterListChannelMessagesFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.ChannelMessageList) error + RuntimeBeforeListFriendsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) (*empty.Empty, error, codes.Code) + RuntimeAfterListFriendsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.Friends) error + RuntimeBeforeAddFriendsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AddFriendsRequest) (*api.AddFriendsRequest, error, codes.Code) + RuntimeAfterAddFriendsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeDeleteFriendsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.DeleteFriendsRequest) (*api.DeleteFriendsRequest, error, codes.Code) + RuntimeAfterDeleteFriendsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeBlockFriendsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.BlockFriendsRequest) (*api.BlockFriendsRequest, error, codes.Code) + RuntimeAfterBlockFriendsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeImportFacebookFriendsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ImportFacebookFriendsRequest) (*api.ImportFacebookFriendsRequest, error, codes.Code) + RuntimeAfterImportFacebookFriendsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeCreateGroupFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.CreateGroupRequest) (*api.CreateGroupRequest, error, codes.Code) + RuntimeAfterCreateGroupFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.Group) error + RuntimeBeforeUpdateGroupFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.UpdateGroupRequest) (*api.UpdateGroupRequest, error, codes.Code) + RuntimeAfterUpdateGroupFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeDeleteGroupFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.DeleteGroupRequest) (*api.DeleteGroupRequest, error, codes.Code) + RuntimeAfterDeleteGroupFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeJoinGroupFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.JoinGroupRequest) (*api.JoinGroupRequest, error, codes.Code) + RuntimeAfterJoinGroupFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeLeaveGroupFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.LeaveGroupRequest) (*api.LeaveGroupRequest, error, codes.Code) + RuntimeAfterLeaveGroupFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeAddGroupUsersFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AddGroupUsersRequest) (*api.AddGroupUsersRequest, error, codes.Code) + RuntimeAfterAddGroupUsersFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeKickGroupUsersFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.KickGroupUsersRequest) (*api.KickGroupUsersRequest, error, codes.Code) + RuntimeAfterKickGroupUsersFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforePromoteGroupUsersFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.PromoteGroupUsersRequest) (*api.PromoteGroupUsersRequest, error, codes.Code) + RuntimeAfterPromoteGroupUsersFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeListGroupUsersFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ListGroupUsersRequest) (*api.ListGroupUsersRequest, error, codes.Code) + RuntimeAfterListGroupUsersFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.GroupUserList) error + RuntimeBeforeListUserGroupsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ListUserGroupsRequest) (*api.ListUserGroupsRequest, error, codes.Code) + RuntimeAfterListUserGroupsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.UserGroupList) error + RuntimeBeforeListGroupsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ListGroupsRequest) (*api.ListGroupsRequest, error, codes.Code) + RuntimeAfterListGroupsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.GroupList) error + RuntimeBeforeDeleteLeaderboardRecordFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.DeleteLeaderboardRecordRequest) (*api.DeleteLeaderboardRecordRequest, error, codes.Code) + RuntimeAfterDeleteLeaderboardRecordFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeListLeaderboardRecordsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ListLeaderboardRecordsRequest) (*api.ListLeaderboardRecordsRequest, error, codes.Code) + RuntimeAfterListLeaderboardRecordsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.LeaderboardRecordList) error + RuntimeBeforeWriteLeaderboardRecordFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.WriteLeaderboardRecordRequest) (*api.WriteLeaderboardRecordRequest, error, codes.Code) + RuntimeAfterWriteLeaderboardRecordFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.LeaderboardRecord) error + RuntimeBeforeLinkCustomFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountCustom) (*api.AccountCustom, error, codes.Code) + RuntimeAfterLinkCustomFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeLinkDeviceFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountDevice) (*api.AccountDevice, error, codes.Code) + RuntimeAfterLinkDeviceFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeLinkEmailFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountEmail) (*api.AccountEmail, error, codes.Code) + RuntimeAfterLinkEmailFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeLinkFacebookFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.LinkFacebookRequest) (*api.LinkFacebookRequest, error, codes.Code) + RuntimeAfterLinkFacebookFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeLinkGameCenterFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountGameCenter) (*api.AccountGameCenter, error, codes.Code) + RuntimeAfterLinkGameCenterFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeLinkGoogleFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountGoogle) (*api.AccountGoogle, error, codes.Code) + RuntimeAfterLinkGoogleFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeLinkSteamFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountSteam) (*api.AccountSteam, error, codes.Code) + RuntimeAfterLinkSteamFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeListMatchesFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ListMatchesRequest) (*api.ListMatchesRequest, error, codes.Code) + RuntimeAfterListMatchesFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.MatchList) error + RuntimeBeforeListNotificationsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ListNotificationsRequest) (*api.ListNotificationsRequest, error, codes.Code) + RuntimeAfterListNotificationsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.NotificationList) error + RuntimeBeforeDeleteNotificationFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.DeleteNotificationsRequest) (*api.DeleteNotificationsRequest, error, codes.Code) + RuntimeAfterDeleteNotificationFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeListStorageObjectsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ListStorageObjectsRequest) (*api.ListStorageObjectsRequest, error, codes.Code) + RuntimeAfterListStorageObjectsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.StorageObjectList) error + RuntimeBeforeReadStorageObjectsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ReadStorageObjectsRequest) (*api.ReadStorageObjectsRequest, error, codes.Code) + RuntimeAfterReadStorageObjectsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.StorageObjects) error + RuntimeBeforeWriteStorageObjectsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.WriteStorageObjectsRequest) (*api.WriteStorageObjectsRequest, error, codes.Code) + RuntimeAfterWriteStorageObjectsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.StorageObjectAcks) error + RuntimeBeforeDeleteStorageObjectsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.DeleteStorageObjectsRequest) (*api.DeleteStorageObjectsRequest, error, codes.Code) + RuntimeAfterDeleteStorageObjectsFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeUnlinkCustomFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountCustom) (*api.AccountCustom, error, codes.Code) + RuntimeAfterUnlinkCustomFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeUnlinkDeviceFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountDevice) (*api.AccountDevice, error, codes.Code) + RuntimeAfterUnlinkDeviceFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeUnlinkEmailFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountEmail) (*api.AccountEmail, error, codes.Code) + RuntimeAfterUnlinkEmailFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeUnlinkFacebookFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountFacebook) (*api.AccountFacebook, error, codes.Code) + RuntimeAfterUnlinkFacebookFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeUnlinkGameCenterFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountGameCenter) (*api.AccountGameCenter, error, codes.Code) + RuntimeAfterUnlinkGameCenterFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeUnlinkGoogleFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountGoogle) (*api.AccountGoogle, error, codes.Code) + RuntimeAfterUnlinkGoogleFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeUnlinkSteamFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountSteam) (*api.AccountSteam, error, codes.Code) + RuntimeAfterUnlinkSteamFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error + RuntimeBeforeGetUsersFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.GetUsersRequest) (*api.GetUsersRequest, error, codes.Code) + RuntimeAfterGetUsersFunction func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.Users) error + + RuntimeMatchmakerMatchedFunction func(entries []*MatchmakerEntry) (string, bool, error) + + RuntimeMatchCreateFunction func(logger *zap.Logger, id uuid.UUID, node string, name string, labelUpdateFn func(string)) (RuntimeMatchCore, error) +) + +type RuntimeExecutionMode int + +const ( + RuntimeExecutionModeRunOnce RuntimeExecutionMode = iota + RuntimeExecutionModeRPC + RuntimeExecutionModeBefore + RuntimeExecutionModeAfter + RuntimeExecutionModeMatch + RuntimeExecutionModeMatchmaker + RuntimeExecutionModeMatchCreate +) + +func (e RuntimeExecutionMode) String() string { + switch e { + case RuntimeExecutionModeRunOnce: + return "run_once" + case RuntimeExecutionModeRPC: + return "rpc" + case RuntimeExecutionModeBefore: + return "before" + case RuntimeExecutionModeAfter: + return "after" + case RuntimeExecutionModeMatch: + return "match" + case RuntimeExecutionModeMatchmaker: + return "matchmaker" + case RuntimeExecutionModeMatchCreate: + return "match_create" } + + return "" } -func (rp *RuntimePool) Put(r *Runtime) { - select { - case rp.poolCh <- r: - // Runtime is successfully returned to the pool. - default: - // The pool is over capacity. Should never happen but guard anyway. - // Safe to continue processing, the runtime is just discarded. - rp.logger.Warn("Runtime pool full, discarding runtime") - } +type RuntimeMatchCore interface { + MatchInit(params map[string]interface{}) (interface{}, int, string, error) + MatchJoinAttempt(tick int64, state interface{}, userID, sessionID uuid.UUID, username, node string) (interface{}, bool, string, error) + MatchJoin(tick int64, state interface{}, joins []*MatchPresence) (interface{}, error) + MatchLeave(tick int64, state interface{}, leaves []*MatchPresence) (interface{}, error) + MatchLoop(tick int64, state interface{}, inputCh chan *MatchDataMessage) (interface{}, error) } -func newVM(logger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, leaderboardCache LeaderboardCache, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter, stdLibs map[string]lua.LGFunction, moduleCache *ModuleCache, once *sync.Once, announceCallback func(ExecutionMode, string)) (*Runtime, error) { - // Initialize a one-off runtime to ensure startup code runs and modules are valid. - vm := lua.NewState(lua.Options{ - CallStackSize: config.GetRuntime().CallStackSize, - RegistrySize: config.GetRuntime().RegistrySize, - SkipOpenLibs: true, - IncludeGoStackTrace: true, - }) - for name, lib := range stdLibs { - vm.Push(vm.NewFunction(lib)) - vm.Push(lua.LString(name)) - vm.Call(1, 0) - } - nakamaModule := NewNakamaModule(logger, db, config, socialClient, leaderboardCache, vm, sessionRegistry, matchRegistry, tracker, router, once, announceCallback) - vm.PreloadModule("nakama", nakamaModule.Loader) - r := &Runtime{ - logger: logger, - vm: vm, - luaEnv: ConvertMap(vm, config.GetRuntime().Environment), - } +type RuntimeBeforeReqFunctions struct { + beforeGetAccountFunction RuntimeBeforeGetAccountFunction + beforeUpdateAccountFunction RuntimeBeforeUpdateAccountFunction + beforeAuthenticateCustomFunction RuntimeBeforeAuthenticateCustomFunction + beforeAuthenticateDeviceFunction RuntimeBeforeAuthenticateDeviceFunction + beforeAuthenticateEmailFunction RuntimeBeforeAuthenticateEmailFunction + beforeAuthenticateFacebookFunction RuntimeBeforeAuthenticateFacebookFunction + beforeAuthenticateGameCenterFunction RuntimeBeforeAuthenticateGameCenterFunction + beforeAuthenticateGoogleFunction RuntimeBeforeAuthenticateGoogleFunction + beforeAuthenticateSteamFunction RuntimeBeforeAuthenticateSteamFunction + beforeListChannelMessagesFunction RuntimeBeforeListChannelMessagesFunction + beforeListFriendsFunction RuntimeBeforeListFriendsFunction + beforeAddFriendsFunction RuntimeBeforeAddFriendsFunction + beforeDeleteFriendsFunction RuntimeBeforeDeleteFriendsFunction + beforeBlockFriendsFunction RuntimeBeforeBlockFriendsFunction + beforeImportFacebookFriendsFunction RuntimeBeforeImportFacebookFriendsFunction + beforeCreateGroupFunction RuntimeBeforeCreateGroupFunction + beforeUpdateGroupFunction RuntimeBeforeUpdateGroupFunction + beforeDeleteGroupFunction RuntimeBeforeDeleteGroupFunction + beforeJoinGroupFunction RuntimeBeforeJoinGroupFunction + beforeLeaveGroupFunction RuntimeBeforeLeaveGroupFunction + beforeAddGroupUsersFunction RuntimeBeforeAddGroupUsersFunction + beforeKickGroupUsersFunction RuntimeBeforeKickGroupUsersFunction + beforePromoteGroupUsersFunction RuntimeBeforePromoteGroupUsersFunction + beforeListGroupUsersFunction RuntimeBeforeListGroupUsersFunction + beforeListUserGroupsFunction RuntimeBeforeListUserGroupsFunction + beforeListGroupsFunction RuntimeBeforeListGroupsFunction + beforeDeleteLeaderboardRecordFunction RuntimeBeforeDeleteLeaderboardRecordFunction + beforeListLeaderboardRecordsFunction RuntimeBeforeListLeaderboardRecordsFunction + beforeWriteLeaderboardRecordFunction RuntimeBeforeWriteLeaderboardRecordFunction + beforeLinkCustomFunction RuntimeBeforeLinkCustomFunction + beforeLinkDeviceFunction RuntimeBeforeLinkDeviceFunction + beforeLinkEmailFunction RuntimeBeforeLinkEmailFunction + beforeLinkFacebookFunction RuntimeBeforeLinkFacebookFunction + beforeLinkGameCenterFunction RuntimeBeforeLinkGameCenterFunction + beforeLinkGoogleFunction RuntimeBeforeLinkGoogleFunction + beforeLinkSteamFunction RuntimeBeforeLinkSteamFunction + beforeListMatchesFunction RuntimeBeforeListMatchesFunction + beforeListNotificationsFunction RuntimeBeforeListNotificationsFunction + beforeDeleteNotificationFunction RuntimeBeforeDeleteNotificationFunction + beforeListStorageObjectsFunction RuntimeBeforeListStorageObjectsFunction + beforeReadStorageObjectsFunction RuntimeBeforeReadStorageObjectsFunction + beforeWriteStorageObjectsFunction RuntimeBeforeWriteStorageObjectsFunction + beforeDeleteStorageObjectsFunction RuntimeBeforeDeleteStorageObjectsFunction + beforeUnlinkCustomFunction RuntimeBeforeUnlinkCustomFunction + beforeUnlinkDeviceFunction RuntimeBeforeUnlinkDeviceFunction + beforeUnlinkEmailFunction RuntimeBeforeUnlinkEmailFunction + beforeUnlinkFacebookFunction RuntimeBeforeUnlinkFacebookFunction + beforeUnlinkGameCenterFunction RuntimeBeforeUnlinkGameCenterFunction + beforeUnlinkGoogleFunction RuntimeBeforeUnlinkGoogleFunction + beforeUnlinkSteamFunction RuntimeBeforeUnlinkSteamFunction + beforeGetUsersFunction RuntimeBeforeGetUsersFunction +} - return r, r.loadModules(moduleCache) +type RuntimeAfterReqFunctions struct { + afterGetAccountFunction RuntimeAfterGetAccountFunction + afterUpdateAccountFunction RuntimeAfterUpdateAccountFunction + afterAuthenticateCustomFunction RuntimeAfterAuthenticateCustomFunction + afterAuthenticateDeviceFunction RuntimeAfterAuthenticateDeviceFunction + afterAuthenticateEmailFunction RuntimeAfterAuthenticateEmailFunction + afterAuthenticateFacebookFunction RuntimeAfterAuthenticateFacebookFunction + afterAuthenticateGameCenterFunction RuntimeAfterAuthenticateGameCenterFunction + afterAuthenticateGoogleFunction RuntimeAfterAuthenticateGoogleFunction + afterAuthenticateSteamFunction RuntimeAfterAuthenticateSteamFunction + afterListChannelMessagesFunction RuntimeAfterListChannelMessagesFunction + afterListFriendsFunction RuntimeAfterListFriendsFunction + afterAddFriendsFunction RuntimeAfterAddFriendsFunction + afterDeleteFriendsFunction RuntimeAfterDeleteFriendsFunction + afterBlockFriendsFunction RuntimeAfterBlockFriendsFunction + afterImportFacebookFriendsFunction RuntimeAfterImportFacebookFriendsFunction + afterCreateGroupFunction RuntimeAfterCreateGroupFunction + afterUpdateGroupFunction RuntimeAfterUpdateGroupFunction + afterDeleteGroupFunction RuntimeAfterDeleteGroupFunction + afterJoinGroupFunction RuntimeAfterJoinGroupFunction + afterLeaveGroupFunction RuntimeAfterLeaveGroupFunction + afterAddGroupUsersFunction RuntimeAfterAddGroupUsersFunction + afterKickGroupUsersFunction RuntimeAfterKickGroupUsersFunction + afterPromoteGroupUsersFunction RuntimeAfterPromoteGroupUsersFunction + afterListGroupUsersFunction RuntimeAfterListGroupUsersFunction + afterListUserGroupsFunction RuntimeAfterListUserGroupsFunction + afterListGroupsFunction RuntimeAfterListGroupsFunction + afterDeleteLeaderboardRecordFunction RuntimeAfterDeleteLeaderboardRecordFunction + afterListLeaderboardRecordsFunction RuntimeAfterListLeaderboardRecordsFunction + afterWriteLeaderboardRecordFunction RuntimeAfterWriteLeaderboardRecordFunction + afterLinkCustomFunction RuntimeAfterLinkCustomFunction + afterLinkDeviceFunction RuntimeAfterLinkDeviceFunction + afterLinkEmailFunction RuntimeAfterLinkEmailFunction + afterLinkFacebookFunction RuntimeAfterLinkFacebookFunction + afterLinkGameCenterFunction RuntimeAfterLinkGameCenterFunction + afterLinkGoogleFunction RuntimeAfterLinkGoogleFunction + afterLinkSteamFunction RuntimeAfterLinkSteamFunction + afterListMatchesFunction RuntimeAfterListMatchesFunction + afterListNotificationsFunction RuntimeAfterListNotificationsFunction + afterDeleteNotificationFunction RuntimeAfterDeleteNotificationFunction + afterListStorageObjectsFunction RuntimeAfterListStorageObjectsFunction + afterReadStorageObjectsFunction RuntimeAfterReadStorageObjectsFunction + afterWriteStorageObjectsFunction RuntimeAfterWriteStorageObjectsFunction + afterDeleteStorageObjectsFunction RuntimeAfterDeleteStorageObjectsFunction + afterUnlinkCustomFunction RuntimeAfterUnlinkCustomFunction + afterUnlinkDeviceFunction RuntimeAfterUnlinkDeviceFunction + afterUnlinkEmailFunction RuntimeAfterUnlinkEmailFunction + afterUnlinkFacebookFunction RuntimeAfterUnlinkFacebookFunction + afterUnlinkGameCenterFunction RuntimeAfterUnlinkGameCenterFunction + afterUnlinkGoogleFunction RuntimeAfterUnlinkGoogleFunction + afterUnlinkSteamFunction RuntimeAfterUnlinkSteamFunction + afterGetUsersFunction RuntimeAfterGetUsersFunction } type Runtime struct { - logger *zap.Logger - vm *lua.LState - luaEnv *lua.LTable -} - -func (r *Runtime) loadModules(moduleCache *ModuleCache) error { - // `DoFile(..)` only parses and evaluates modules. Calling it multiple times, will load and eval the file multiple times. - // So to make sure that we only load and evaluate modules once, regardless of whether there is dependency between files, we load them all into `preload`. - // This is to make sure that modules are only loaded and evaluated once as `doFile()` does not (always) update _LOADED table. - // Bear in mind two separate thoughts around the script runtime design choice: - // - // 1) This is only a problem if one module is dependent on another module. - // This means that the global functions are evaluated once at system startup and then later on when the module is required through `require`. - // We circumvent this by checking the _LOADED table to check if `require` had evaluated the module and avoiding double-eval. - // - // 2) Second item is that modules must be pre-loaded into the state for callback-func eval to work properly (in case of HTTP/RPC/etc invokes) - // So we need to always load the modules into the system via `preload` so that they are always available in the LState. - // We can't rely on `require` to have seen the module in case there is no dependency between the modules. - - //for _, mod := range r.modules { - // relPath, _ := filepath.Rel(r.luaPath, mod) - // moduleName := strings.TrimSuffix(relPath, filepath.Ext(relPath)) - // - // // check to see if this module was loaded by `require` before executing it - // loaded := l.GetField(l.Get(lua.RegistryIndex), "_LOADED") - // lv := l.GetField(loaded, moduleName) - // if lua.LVAsBool(lv) { - // // Already evaluated module via `require(..)` - // continue - // } - // - // if err = l.DoFile(mod); err != nil { - // failedModules++ - // r.logger.Error("Failed to evaluate module - skipping", zap.String("path", mod), zap.Error(err)) - // } - //} - - preload := r.vm.GetField(r.vm.GetField(r.vm.Get(lua.EnvironIndex), "package"), "preload") - fns := make(map[string]*lua.LFunction) - for _, name := range moduleCache.Names { - module, ok := moduleCache.Modules[name] - if !ok { - r.logger.Fatal("Failed to find named module in cache", zap.String("name", name)) - } - f, err := r.vm.Load(bytes.NewReader(module.Content), module.Path) - if err != nil { - r.logger.Error("Could not load module", zap.String("name", module.Path), zap.Error(err)) - return err - } else { - r.vm.SetField(preload, module.Name, f) - fns[module.Name] = f - } - } + rpcFunctions map[string]RuntimeRpcFunction - for _, name := range moduleCache.Names { - fn, ok := fns[name] - if !ok { - r.logger.Fatal("Failed to find named module in prepared functions", zap.String("name", name)) - } - loaded := r.vm.GetField(r.vm.Get(lua.RegistryIndex), "_LOADED") - lv := r.vm.GetField(loaded, name) - if lua.LVAsBool(lv) { - // Already evaluated module via `require(..)` - continue - } + beforeRtFunctions map[string]RuntimeBeforeRtFunction + afterRtFunctions map[string]RuntimeAfterRtFunction - r.vm.Push(fn) - fnErr := r.vm.PCall(0, -1, nil) - if fnErr != nil { - r.logger.Error("Could not complete runtime invocation", zap.Error(fnErr)) - return fnErr - } - } + beforeReqFunctions *RuntimeBeforeReqFunctions + afterReqFunctions *RuntimeAfterReqFunctions - return nil + matchmakerMatchedFunction RuntimeMatchmakerMatchedFunction } -func (r *Runtime) NewStateThread() (*lua.LState, context.CancelFunc) { - return r.vm, nil -} +func NewRuntime(logger, startupLogger *zap.Logger, db *sql.DB, jsonpbMarshaler *jsonpb.Marshaler, jsonpbUnmarshaler *jsonpb.Unmarshaler, config Config, socialClient *social.Client, leaderboardCache LeaderboardCache, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter) (*Runtime, error) { + runtimeConfig := config.GetRuntime() + startupLogger.Info("Initialising runtime", zap.String("path", runtimeConfig.Path)) -func (r *Runtime) GetCallback(e ExecutionMode, key string) *lua.LFunction { - cp := r.vm.Context().Value(CALLBACKS).(*Callbacks) - switch e { - case ExecutionModeRPC: - return cp.RPC[key] - case ExecutionModeBefore: - return cp.Before[key] - case ExecutionModeAfter: - return cp.After[key] - case ExecutionModeMatchmaker: - return cp.Matchmaker + if err := os.MkdirAll(runtimeConfig.Path, os.ModePerm); err != nil { + return nil, err } - return nil -} + paths := make([]string, 0) + if err := filepath.Walk(runtimeConfig.Path, func(path string, f os.FileInfo, err error) error { + if err != nil { + startupLogger.Error("Error listing runtime path", zap.String("path", path), zap.Error(err)) + return err + } -func (r *Runtime) InvokeFunction(execMode ExecutionMode, fn *lua.LFunction, queryParams map[string][]string, uid string, username string, sessionExpiry int64, sid string, clientIP string, clientPort string, payload interface{}) (interface{}, error, codes.Code) { - ctx := NewLuaContext(r.vm, r.luaEnv, execMode, queryParams, sessionExpiry, uid, username, sid, clientIP, clientPort) - var lv lua.LValue - if payload != nil { - lv = ConvertValue(r.vm, payload) + // Ignore directories. + if !f.IsDir() { + paths = append(paths, path) + } + return nil + }); err != nil { + startupLogger.Error("Failed to list runtime path", zap.Error(err)) + return nil, err } - retValue, err, code := r.invokeFunction(r.vm, fn, ctx, lv) + goModules, goRpcFunctions, goBeforeRtFunctions, goAfterRtFunctions, goBeforeReqFunctions, goAfterReqFunctions, goMatchmakerMatchedFunction, goMatchCreateFn, goSetMatchCreateFn, goMatchNamesListFn, err := NewRuntimeProviderGo(logger, startupLogger, db, config, socialClient, leaderboardCache, sessionRegistry, matchRegistry, tracker, router, runtimeConfig.Path, paths) if err != nil { - return nil, err, code + startupLogger.Error("Error initialising Go runtime provider", zap.Error(err)) + return nil, err } - if retValue == nil || retValue == lua.LNil { - return nil, nil, 0 + luaModules, luaRpcFunctions, luaBeforeRtFunctions, luaAfterRtFunctions, luaBeforeReqFunctions, luaAfterReqFunctions, luaMatchmakerMatchedFunction, allMatchCreateFn, err := NewRuntimeProviderLua(logger, startupLogger, db, jsonpbMarshaler, jsonpbUnmarshaler, config, socialClient, leaderboardCache, sessionRegistry, matchRegistry, tracker, router, goMatchCreateFn, runtimeConfig.Path, paths) + if err != nil { + startupLogger.Error("Error initialising Lua runtime provider", zap.Error(err)) + return nil, err } - return ConvertLuaValue(retValue), nil, 0 -} + // allMatchCreateFn has already been set up by the Lua side to multiplex, now tell the Go side to use it too. + goSetMatchCreateFn(allMatchCreateFn) -func (r *Runtime) invokeFunction(l *lua.LState, fn *lua.LFunction, ctx *lua.LTable, payload lua.LValue) (lua.LValue, error, codes.Code) { - l.Push(LSentinel) - l.Push(fn) + allModules := make([]string, 0, len(goModules)+len(luaModules)) + for _, module := range luaModules { + allModules = append(allModules, module) + } + for _, module := range goModules { + allModules = append(allModules, module) + } + startupLogger.Info("Found runtime modules", zap.Int("count", len(allModules)), zap.Strings("modules", allModules)) - nargs := 1 - l.Push(ctx) + allRpcFunctions := make(map[string]RuntimeRpcFunction, len(goRpcFunctions)+len(luaRpcFunctions)) + for id, fn := range luaRpcFunctions { + allRpcFunctions[id] = fn + startupLogger.Info("Registered Lua runtime RPC function invocation", zap.String("id", id)) + } + for id, fn := range goRpcFunctions { + allRpcFunctions[id] = fn + startupLogger.Info("Registered Go runtime RPC function invocation", zap.String("id", id)) + } - if payload != nil { - nargs = 2 - l.Push(payload) + allBeforeRtFunctions := make(map[string]RuntimeBeforeRtFunction, len(goBeforeRtFunctions)+len(luaBeforeRtFunctions)) + for id, fn := range luaBeforeRtFunctions { + allBeforeRtFunctions[id] = fn + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", strings.TrimPrefix(strings.TrimPrefix(id, API_PREFIX), RTAPI_PREFIX))) + } + for id, fn := range goBeforeRtFunctions { + allBeforeRtFunctions[id] = fn + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", strings.TrimPrefix(strings.TrimPrefix(id, API_PREFIX), RTAPI_PREFIX))) } - err := l.PCall(nargs, lua.MultRet, nil) - if err != nil { - // Unwind the stack up to and including our sentinel value, effectively discarding any other returned parameters. - for { - v := l.Get(-1) - l.Pop(1) - if v.Type() == LTSentinel { - break - } - } + allAfterRtFunctions := make(map[string]RuntimeAfterRtFunction, len(goAfterRtFunctions)+len(luaAfterRtFunctions)) + for id, fn := range luaAfterRtFunctions { + allAfterRtFunctions[id] = fn + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", strings.TrimPrefix(strings.TrimPrefix(id, API_PREFIX), RTAPI_PREFIX))) + } + for id, fn := range goAfterRtFunctions { + allAfterRtFunctions[id] = fn + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", strings.TrimPrefix(strings.TrimPrefix(id, API_PREFIX), RTAPI_PREFIX))) + } - if apiError, ok := err.(*lua.ApiError); ok && apiError.Object.Type() == lua.LTTable { - t := apiError.Object.(*lua.LTable) - switch t.Len() { - case 0: - return nil, err, codes.Internal - case 1: - apiError.Object = t.RawGetInt(1) - return nil, err, codes.Internal - default: - // Ignore everything beyond the first 2 params, if there are more. - apiError.Object = t.RawGetInt(1) - code := codes.Internal - if c := t.RawGetInt(2); c.Type() == lua.LTNumber { - code = codes.Code(c.(lua.LNumber)) - } - return nil, err, code - } - } + allBeforeReqFunctions := luaBeforeReqFunctions + if allBeforeReqFunctions.beforeGetAccountFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "getaccount")) + } + if allBeforeReqFunctions.beforeUpdateAccountFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "updateaccount")) + } + if allBeforeReqFunctions.beforeAuthenticateCustomFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "authenticatecustom")) + } + if allBeforeReqFunctions.beforeAuthenticateDeviceFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "authenticatedevice")) + } + if allBeforeReqFunctions.beforeAuthenticateEmailFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "authenticateemail")) + } + if allBeforeReqFunctions.beforeAuthenticateFacebookFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "authenticatefacebook")) + } + if allBeforeReqFunctions.beforeAuthenticateGameCenterFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "authenticategamecenter")) + } + if allBeforeReqFunctions.beforeAuthenticateGoogleFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "authenticategoogle")) + } + if allBeforeReqFunctions.beforeAuthenticateSteamFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "authenticatesteam")) + } + if allBeforeReqFunctions.beforeListChannelMessagesFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "listchannelmessages")) + } + if allBeforeReqFunctions.beforeListFriendsFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "listfriends")) + } + if allBeforeReqFunctions.beforeAddFriendsFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "addfriends")) + } + if allBeforeReqFunctions.beforeDeleteFriendsFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "deletefriends")) + } + if allBeforeReqFunctions.beforeBlockFriendsFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "blockfriends")) + } + if allBeforeReqFunctions.beforeImportFacebookFriendsFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "importfacebookfriends")) + } + if allBeforeReqFunctions.beforeCreateGroupFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "creategroup")) + } + if allBeforeReqFunctions.beforeUpdateGroupFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "updategroup")) + } + if allBeforeReqFunctions.beforeDeleteGroupFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "deletegroup")) + } + if allBeforeReqFunctions.beforeJoinGroupFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "joingroup")) + } + if allBeforeReqFunctions.beforeLeaveGroupFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "leavegroup")) + } + if allBeforeReqFunctions.beforeAddGroupUsersFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "addgroupusers")) + } + if allBeforeReqFunctions.beforeKickGroupUsersFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "kickgroupusers")) + } + if allBeforeReqFunctions.beforePromoteGroupUsersFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "promotegroupusers")) + } + if allBeforeReqFunctions.beforeListGroupUsersFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "listgroupusers")) + } + if allBeforeReqFunctions.beforeListUserGroupsFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "listusergroups")) + } + if allBeforeReqFunctions.beforeListGroupsFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "listgroups")) + } + if allBeforeReqFunctions.beforeDeleteLeaderboardRecordFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "deleteleaderboardrecord")) + } + if allBeforeReqFunctions.beforeListLeaderboardRecordsFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "listleaderboardrecords")) + } + if allBeforeReqFunctions.beforeWriteLeaderboardRecordFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "writeleaderboardrecord")) + } + if allBeforeReqFunctions.beforeLinkCustomFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "linkcustom")) + } + if allBeforeReqFunctions.beforeLinkDeviceFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "linkdevice")) + } + if allBeforeReqFunctions.beforeLinkEmailFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "linkemail")) + } + if allBeforeReqFunctions.beforeLinkFacebookFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "linkfacebook")) + } + if allBeforeReqFunctions.beforeLinkGameCenterFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "linkgamecenter")) + } + if allBeforeReqFunctions.beforeLinkGoogleFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "linkgoogle")) + } + if allBeforeReqFunctions.beforeLinkSteamFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "linksteam")) + } + if allBeforeReqFunctions.beforeListMatchesFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "listmatches")) + } + if allBeforeReqFunctions.beforeListNotificationsFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "listnotifications")) + } + if allBeforeReqFunctions.beforeDeleteNotificationFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "deletenotification")) + } + if allBeforeReqFunctions.beforeListStorageObjectsFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "liststorageobjects")) + } + if allBeforeReqFunctions.beforeReadStorageObjectsFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "readstorageobjects")) + } + if allBeforeReqFunctions.beforeWriteStorageObjectsFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "writestorageobjects")) + } + if allBeforeReqFunctions.beforeDeleteStorageObjectsFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "deletestorageobjects")) + } + if allBeforeReqFunctions.beforeUnlinkCustomFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "unlinkcustom")) + } + if allBeforeReqFunctions.beforeUnlinkDeviceFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "unlinkdevice")) + } + if allBeforeReqFunctions.beforeUnlinkEmailFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "unlinkemail")) + } + if allBeforeReqFunctions.beforeUnlinkFacebookFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "unlinkfacebook")) + } + if allBeforeReqFunctions.beforeUnlinkGameCenterFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "unlinkgamecenter")) + } + if allBeforeReqFunctions.beforeUnlinkGoogleFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "unlinkgoogle")) + } + if allBeforeReqFunctions.beforeUnlinkSteamFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "unlinksteam")) + } + if allBeforeReqFunctions.beforeGetUsersFunction != nil { + startupLogger.Info("Registered Lua runtime Before function invocation", zap.String("id", "getusers")) + } + if goBeforeReqFunctions.beforeGetAccountFunction != nil { + allBeforeReqFunctions.beforeGetAccountFunction = goBeforeReqFunctions.beforeGetAccountFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "getaccount")) + } + if goBeforeReqFunctions.beforeUpdateAccountFunction != nil { + allBeforeReqFunctions.beforeUpdateAccountFunction = goBeforeReqFunctions.beforeUpdateAccountFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "updateaccount")) + } + if goBeforeReqFunctions.beforeAuthenticateCustomFunction != nil { + allBeforeReqFunctions.beforeAuthenticateCustomFunction = goBeforeReqFunctions.beforeAuthenticateCustomFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "authenticatecustom")) + } + if goBeforeReqFunctions.beforeAuthenticateDeviceFunction != nil { + allBeforeReqFunctions.beforeAuthenticateDeviceFunction = goBeforeReqFunctions.beforeAuthenticateDeviceFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "authenticatedevice")) + } + if goBeforeReqFunctions.beforeAuthenticateEmailFunction != nil { + allBeforeReqFunctions.beforeAuthenticateEmailFunction = goBeforeReqFunctions.beforeAuthenticateEmailFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "authenticateemail")) + } + if goBeforeReqFunctions.beforeAuthenticateFacebookFunction != nil { + allBeforeReqFunctions.beforeAuthenticateFacebookFunction = goBeforeReqFunctions.beforeAuthenticateFacebookFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "authenticatefacebook")) + } + if goBeforeReqFunctions.beforeAuthenticateGameCenterFunction != nil { + allBeforeReqFunctions.beforeAuthenticateGameCenterFunction = goBeforeReqFunctions.beforeAuthenticateGameCenterFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "authenticategamecenter")) + } + if goBeforeReqFunctions.beforeAuthenticateGoogleFunction != nil { + allBeforeReqFunctions.beforeAuthenticateGoogleFunction = goBeforeReqFunctions.beforeAuthenticateGoogleFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "authenticategoogle")) + } + if goBeforeReqFunctions.beforeAuthenticateSteamFunction != nil { + allBeforeReqFunctions.beforeAuthenticateSteamFunction = goBeforeReqFunctions.beforeAuthenticateSteamFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "authenticatesteam")) + } + if goBeforeReqFunctions.beforeListChannelMessagesFunction != nil { + allBeforeReqFunctions.beforeListChannelMessagesFunction = goBeforeReqFunctions.beforeListChannelMessagesFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "listchannelmessages")) + } + if goBeforeReqFunctions.beforeListFriendsFunction != nil { + allBeforeReqFunctions.beforeListFriendsFunction = goBeforeReqFunctions.beforeListFriendsFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "listfriends")) + } + if goBeforeReqFunctions.beforeAddFriendsFunction != nil { + allBeforeReqFunctions.beforeAddFriendsFunction = goBeforeReqFunctions.beforeAddFriendsFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "addfriends")) + } + if goBeforeReqFunctions.beforeDeleteFriendsFunction != nil { + allBeforeReqFunctions.beforeDeleteFriendsFunction = goBeforeReqFunctions.beforeDeleteFriendsFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "deletefriends")) + } + if goBeforeReqFunctions.beforeBlockFriendsFunction != nil { + allBeforeReqFunctions.beforeBlockFriendsFunction = goBeforeReqFunctions.beforeBlockFriendsFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "blockfriends")) + } + if goBeforeReqFunctions.beforeImportFacebookFriendsFunction != nil { + allBeforeReqFunctions.beforeImportFacebookFriendsFunction = goBeforeReqFunctions.beforeImportFacebookFriendsFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "importfacebookfriends")) + } + if goBeforeReqFunctions.beforeCreateGroupFunction != nil { + allBeforeReqFunctions.beforeCreateGroupFunction = goBeforeReqFunctions.beforeCreateGroupFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "creategroup")) + } + if goBeforeReqFunctions.beforeUpdateGroupFunction != nil { + allBeforeReqFunctions.beforeUpdateGroupFunction = goBeforeReqFunctions.beforeUpdateGroupFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "updategroup")) + } + if goBeforeReqFunctions.beforeDeleteGroupFunction != nil { + allBeforeReqFunctions.beforeDeleteGroupFunction = goBeforeReqFunctions.beforeDeleteGroupFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "deletegroup")) + } + if goBeforeReqFunctions.beforeJoinGroupFunction != nil { + allBeforeReqFunctions.beforeJoinGroupFunction = goBeforeReqFunctions.beforeJoinGroupFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "joingroup")) + } + if goBeforeReqFunctions.beforeLeaveGroupFunction != nil { + allBeforeReqFunctions.beforeLeaveGroupFunction = goBeforeReqFunctions.beforeLeaveGroupFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "leavegroup")) + } + if goBeforeReqFunctions.beforeAddGroupUsersFunction != nil { + allBeforeReqFunctions.beforeAddGroupUsersFunction = goBeforeReqFunctions.beforeAddGroupUsersFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "addgroupusers")) + } + if goBeforeReqFunctions.beforeKickGroupUsersFunction != nil { + allBeforeReqFunctions.beforeKickGroupUsersFunction = goBeforeReqFunctions.beforeKickGroupUsersFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "kickgroupusers")) + } + if goBeforeReqFunctions.beforePromoteGroupUsersFunction != nil { + allBeforeReqFunctions.beforePromoteGroupUsersFunction = goBeforeReqFunctions.beforePromoteGroupUsersFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "promotegroupusers")) + } + if goBeforeReqFunctions.beforeListGroupUsersFunction != nil { + allBeforeReqFunctions.beforeListGroupUsersFunction = goBeforeReqFunctions.beforeListGroupUsersFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "listgroupusers")) + } + if goBeforeReqFunctions.beforeListUserGroupsFunction != nil { + allBeforeReqFunctions.beforeListUserGroupsFunction = goBeforeReqFunctions.beforeListUserGroupsFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "listusergroups")) + } + if goBeforeReqFunctions.beforeListGroupsFunction != nil { + allBeforeReqFunctions.beforeListGroupsFunction = goBeforeReqFunctions.beforeListGroupsFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "listgroups")) + } + if goBeforeReqFunctions.beforeDeleteLeaderboardRecordFunction != nil { + allBeforeReqFunctions.beforeDeleteLeaderboardRecordFunction = goBeforeReqFunctions.beforeDeleteLeaderboardRecordFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "deleteleaderboardrecord")) + } + if goBeforeReqFunctions.beforeListLeaderboardRecordsFunction != nil { + allBeforeReqFunctions.beforeListLeaderboardRecordsFunction = goBeforeReqFunctions.beforeListLeaderboardRecordsFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "listleaderboardrecords")) + } + if goBeforeReqFunctions.beforeWriteLeaderboardRecordFunction != nil { + allBeforeReqFunctions.beforeWriteLeaderboardRecordFunction = goBeforeReqFunctions.beforeWriteLeaderboardRecordFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "writeleaderboardrecord")) + } + if goBeforeReqFunctions.beforeLinkCustomFunction != nil { + allBeforeReqFunctions.beforeLinkCustomFunction = goBeforeReqFunctions.beforeLinkCustomFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "linkcustom")) + } + if goBeforeReqFunctions.beforeLinkDeviceFunction != nil { + allBeforeReqFunctions.beforeLinkDeviceFunction = goBeforeReqFunctions.beforeLinkDeviceFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "linkdevice")) + } + if goBeforeReqFunctions.beforeLinkEmailFunction != nil { + allBeforeReqFunctions.beforeLinkEmailFunction = goBeforeReqFunctions.beforeLinkEmailFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "linkemail")) + } + if goBeforeReqFunctions.beforeLinkFacebookFunction != nil { + allBeforeReqFunctions.beforeLinkFacebookFunction = goBeforeReqFunctions.beforeLinkFacebookFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "linkfacebook")) + } + if goBeforeReqFunctions.beforeLinkGameCenterFunction != nil { + allBeforeReqFunctions.beforeLinkGameCenterFunction = goBeforeReqFunctions.beforeLinkGameCenterFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "linkgamecenter")) + } + if goBeforeReqFunctions.beforeLinkGoogleFunction != nil { + allBeforeReqFunctions.beforeLinkGoogleFunction = goBeforeReqFunctions.beforeLinkGoogleFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "linkgoogle")) + } + if goBeforeReqFunctions.beforeLinkSteamFunction != nil { + allBeforeReqFunctions.beforeLinkSteamFunction = goBeforeReqFunctions.beforeLinkSteamFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "linksteam")) + } + if goBeforeReqFunctions.beforeListMatchesFunction != nil { + allBeforeReqFunctions.beforeListMatchesFunction = goBeforeReqFunctions.beforeListMatchesFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "listmatches")) + } + if goBeforeReqFunctions.beforeListNotificationsFunction != nil { + allBeforeReqFunctions.beforeListNotificationsFunction = goBeforeReqFunctions.beforeListNotificationsFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "listnotifications")) + } + if goBeforeReqFunctions.beforeDeleteNotificationFunction != nil { + allBeforeReqFunctions.beforeDeleteNotificationFunction = goBeforeReqFunctions.beforeDeleteNotificationFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "deletenotification")) + } + if goBeforeReqFunctions.beforeListStorageObjectsFunction != nil { + allBeforeReqFunctions.beforeListStorageObjectsFunction = goBeforeReqFunctions.beforeListStorageObjectsFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "liststorageobjects")) + } + if goBeforeReqFunctions.beforeReadStorageObjectsFunction != nil { + allBeforeReqFunctions.beforeReadStorageObjectsFunction = goBeforeReqFunctions.beforeReadStorageObjectsFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "readstorageobjects")) + } + if goBeforeReqFunctions.beforeWriteStorageObjectsFunction != nil { + allBeforeReqFunctions.beforeWriteStorageObjectsFunction = goBeforeReqFunctions.beforeWriteStorageObjectsFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "writestorageobjects")) + } + if goBeforeReqFunctions.beforeDeleteStorageObjectsFunction != nil { + allBeforeReqFunctions.beforeDeleteStorageObjectsFunction = goBeforeReqFunctions.beforeDeleteStorageObjectsFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "deletestorageobjects")) + } + if goBeforeReqFunctions.beforeUnlinkCustomFunction != nil { + allBeforeReqFunctions.beforeUnlinkCustomFunction = goBeforeReqFunctions.beforeUnlinkCustomFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "unlinkcustom")) + } + if goBeforeReqFunctions.beforeUnlinkDeviceFunction != nil { + allBeforeReqFunctions.beforeUnlinkDeviceFunction = goBeforeReqFunctions.beforeUnlinkDeviceFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "unlinkdevice")) + } + if goBeforeReqFunctions.beforeUnlinkEmailFunction != nil { + allBeforeReqFunctions.beforeUnlinkEmailFunction = goBeforeReqFunctions.beforeUnlinkEmailFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "unlinkemail")) + } + if goBeforeReqFunctions.beforeUnlinkFacebookFunction != nil { + allBeforeReqFunctions.beforeUnlinkFacebookFunction = goBeforeReqFunctions.beforeUnlinkFacebookFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "unlinkfacebook")) + } + if goBeforeReqFunctions.beforeUnlinkGameCenterFunction != nil { + allBeforeReqFunctions.beforeUnlinkGameCenterFunction = goBeforeReqFunctions.beforeUnlinkGameCenterFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "unlinkgamecenter")) + } + if goBeforeReqFunctions.beforeUnlinkGoogleFunction != nil { + allBeforeReqFunctions.beforeUnlinkGoogleFunction = goBeforeReqFunctions.beforeUnlinkGoogleFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "unlinkgoogle")) + } + if goBeforeReqFunctions.beforeUnlinkSteamFunction != nil { + allBeforeReqFunctions.beforeUnlinkSteamFunction = goBeforeReqFunctions.beforeUnlinkSteamFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "unlinksteam")) + } + if goBeforeReqFunctions.beforeGetUsersFunction != nil { + allBeforeReqFunctions.beforeGetUsersFunction = goBeforeReqFunctions.beforeGetUsersFunction + startupLogger.Info("Registered Go runtime Before function invocation", zap.String("id", "getusers")) + } - return nil, err, codes.Internal + allAfterReqFunctions := luaAfterReqFunctions + if allAfterReqFunctions.afterGetAccountFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "getaccount")) + } + if allAfterReqFunctions.afterUpdateAccountFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "updateaccount")) + } + if allAfterReqFunctions.afterAuthenticateCustomFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "authenticatecustom")) + } + if allAfterReqFunctions.afterAuthenticateDeviceFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "authenticatedevice")) + } + if allAfterReqFunctions.afterAuthenticateEmailFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "authenticateemail")) + } + if allAfterReqFunctions.afterAuthenticateFacebookFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "authenticatefacebook")) + } + if allAfterReqFunctions.afterAuthenticateGameCenterFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "authenticategamecenter")) + } + if allAfterReqFunctions.afterAuthenticateGoogleFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "authenticategoogle")) + } + if allAfterReqFunctions.afterAuthenticateSteamFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "authenticatesteam")) + } + if allAfterReqFunctions.afterListChannelMessagesFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "listchannelmessages")) + } + if allAfterReqFunctions.afterListFriendsFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "listfriends")) + } + if allAfterReqFunctions.afterAddFriendsFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "addfriends")) + } + if allAfterReqFunctions.afterDeleteFriendsFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "deletefriends")) + } + if allAfterReqFunctions.afterBlockFriendsFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "blockfriends")) + } + if allAfterReqFunctions.afterImportFacebookFriendsFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "importfacebookfriends")) + } + if allAfterReqFunctions.afterCreateGroupFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "creategroup")) + } + if allAfterReqFunctions.afterUpdateGroupFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "updategroup")) + } + if allAfterReqFunctions.afterDeleteGroupFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "deletegroup")) + } + if allAfterReqFunctions.afterJoinGroupFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "joingroup")) + } + if allAfterReqFunctions.afterLeaveGroupFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "leavegroup")) + } + if allAfterReqFunctions.afterAddGroupUsersFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "addgroupusers")) + } + if allAfterReqFunctions.afterKickGroupUsersFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "kickgroupusers")) + } + if allAfterReqFunctions.afterPromoteGroupUsersFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "promotegroupusers")) + } + if allAfterReqFunctions.afterListGroupUsersFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "listgroupusers")) + } + if allAfterReqFunctions.afterListUserGroupsFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "listusergroups")) + } + if allAfterReqFunctions.afterListGroupsFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "listgroups")) + } + if allAfterReqFunctions.afterDeleteLeaderboardRecordFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "deleteleaderboardrecord")) + } + if allAfterReqFunctions.afterListLeaderboardRecordsFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "listleaderboardrecords")) + } + if allAfterReqFunctions.afterWriteLeaderboardRecordFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "writeleaderboardrecord")) + } + if allAfterReqFunctions.afterLinkCustomFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "linkcustom")) + } + if allAfterReqFunctions.afterLinkDeviceFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "linkdevice")) + } + if allAfterReqFunctions.afterLinkEmailFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "linkemail")) + } + if allAfterReqFunctions.afterLinkFacebookFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "linkfacebook")) + } + if allAfterReqFunctions.afterLinkGameCenterFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "linkgamecenter")) + } + if allAfterReqFunctions.afterLinkGoogleFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "linkgoogle")) + } + if allAfterReqFunctions.afterLinkSteamFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "linksteam")) + } + if allAfterReqFunctions.afterListMatchesFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "listmatches")) + } + if allAfterReqFunctions.afterListNotificationsFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "listnotifications")) + } + if allAfterReqFunctions.afterDeleteNotificationFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "deletenotification")) + } + if allAfterReqFunctions.afterListStorageObjectsFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "liststorageobjects")) + } + if allAfterReqFunctions.afterReadStorageObjectsFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "readstorageobjects")) + } + if allAfterReqFunctions.afterWriteStorageObjectsFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "writestorageobjects")) + } + if allAfterReqFunctions.afterDeleteStorageObjectsFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "deletestorageobjects")) + } + if allAfterReqFunctions.afterUnlinkCustomFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "unlinkcustom")) + } + if allAfterReqFunctions.afterUnlinkDeviceFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "unlinkdevice")) + } + if allAfterReqFunctions.afterUnlinkEmailFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "unlinkemail")) + } + if allAfterReqFunctions.afterUnlinkFacebookFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "unlinkfacebook")) + } + if allAfterReqFunctions.afterUnlinkGameCenterFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "unlinkgamecenter")) + } + if allAfterReqFunctions.afterUnlinkGoogleFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "unlinkgoogle")) + } + if allAfterReqFunctions.afterUnlinkSteamFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "unlinksteam")) + } + if allAfterReqFunctions.afterGetUsersFunction != nil { + startupLogger.Info("Registered Lua runtime After function invocation", zap.String("id", "getusers")) + } + if goAfterReqFunctions.afterGetAccountFunction != nil { + allAfterReqFunctions.afterGetAccountFunction = goAfterReqFunctions.afterGetAccountFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "getaccount")) + } + if goAfterReqFunctions.afterUpdateAccountFunction != nil { + allAfterReqFunctions.afterUpdateAccountFunction = goAfterReqFunctions.afterUpdateAccountFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "updateaccount")) + } + if goAfterReqFunctions.afterAuthenticateCustomFunction != nil { + allAfterReqFunctions.afterAuthenticateCustomFunction = goAfterReqFunctions.afterAuthenticateCustomFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "authenticatecustom")) + } + if goAfterReqFunctions.afterAuthenticateDeviceFunction != nil { + allAfterReqFunctions.afterAuthenticateDeviceFunction = goAfterReqFunctions.afterAuthenticateDeviceFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "authenticatedevice")) + } + if goAfterReqFunctions.afterAuthenticateEmailFunction != nil { + allAfterReqFunctions.afterAuthenticateEmailFunction = goAfterReqFunctions.afterAuthenticateEmailFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "authenticateemail")) + } + if goAfterReqFunctions.afterAuthenticateFacebookFunction != nil { + allAfterReqFunctions.afterAuthenticateFacebookFunction = goAfterReqFunctions.afterAuthenticateFacebookFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "authenticatefacebook")) + } + if goAfterReqFunctions.afterAuthenticateGameCenterFunction != nil { + allAfterReqFunctions.afterAuthenticateGameCenterFunction = goAfterReqFunctions.afterAuthenticateGameCenterFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "authenticategamecenter")) + } + if goAfterReqFunctions.afterAuthenticateGoogleFunction != nil { + allAfterReqFunctions.afterAuthenticateGoogleFunction = goAfterReqFunctions.afterAuthenticateGoogleFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "authenticategoogle")) + } + if goAfterReqFunctions.afterAuthenticateSteamFunction != nil { + allAfterReqFunctions.afterAuthenticateSteamFunction = goAfterReqFunctions.afterAuthenticateSteamFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "authenticatesteam")) + } + if goAfterReqFunctions.afterListChannelMessagesFunction != nil { + allAfterReqFunctions.afterListChannelMessagesFunction = goAfterReqFunctions.afterListChannelMessagesFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "listchannelmessages")) + } + if goAfterReqFunctions.afterListFriendsFunction != nil { + allAfterReqFunctions.afterListFriendsFunction = goAfterReqFunctions.afterListFriendsFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "listfriends")) + } + if goAfterReqFunctions.afterAddFriendsFunction != nil { + allAfterReqFunctions.afterAddFriendsFunction = goAfterReqFunctions.afterAddFriendsFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "addfriends")) + } + if goAfterReqFunctions.afterDeleteFriendsFunction != nil { + allAfterReqFunctions.afterDeleteFriendsFunction = goAfterReqFunctions.afterDeleteFriendsFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "deletefriends")) + } + if goAfterReqFunctions.afterBlockFriendsFunction != nil { + allAfterReqFunctions.afterBlockFriendsFunction = goAfterReqFunctions.afterBlockFriendsFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "blockfriends")) + } + if goAfterReqFunctions.afterImportFacebookFriendsFunction != nil { + allAfterReqFunctions.afterImportFacebookFriendsFunction = goAfterReqFunctions.afterImportFacebookFriendsFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "importfacebookfriends")) + } + if goAfterReqFunctions.afterCreateGroupFunction != nil { + allAfterReqFunctions.afterCreateGroupFunction = goAfterReqFunctions.afterCreateGroupFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "creategroup")) + } + if goAfterReqFunctions.afterUpdateGroupFunction != nil { + allAfterReqFunctions.afterUpdateGroupFunction = goAfterReqFunctions.afterUpdateGroupFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "updategroup")) + } + if goAfterReqFunctions.afterDeleteGroupFunction != nil { + allAfterReqFunctions.afterDeleteGroupFunction = goAfterReqFunctions.afterDeleteGroupFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "deletegroup")) + } + if goAfterReqFunctions.afterJoinGroupFunction != nil { + allAfterReqFunctions.afterJoinGroupFunction = goAfterReqFunctions.afterJoinGroupFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "joingroup")) + } + if goAfterReqFunctions.afterLeaveGroupFunction != nil { + allAfterReqFunctions.afterLeaveGroupFunction = goAfterReqFunctions.afterLeaveGroupFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "leavegroup")) + } + if goAfterReqFunctions.afterAddGroupUsersFunction != nil { + allAfterReqFunctions.afterAddGroupUsersFunction = goAfterReqFunctions.afterAddGroupUsersFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "addgroupusers")) + } + if goAfterReqFunctions.afterKickGroupUsersFunction != nil { + allAfterReqFunctions.afterKickGroupUsersFunction = goAfterReqFunctions.afterKickGroupUsersFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "kickgroupusers")) + } + if goAfterReqFunctions.afterPromoteGroupUsersFunction != nil { + allAfterReqFunctions.afterPromoteGroupUsersFunction = goAfterReqFunctions.afterPromoteGroupUsersFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "promotegroupusers")) + } + if goAfterReqFunctions.afterListGroupUsersFunction != nil { + allAfterReqFunctions.afterListGroupUsersFunction = goAfterReqFunctions.afterListGroupUsersFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "listgroupusers")) + } + if goAfterReqFunctions.afterListUserGroupsFunction != nil { + allAfterReqFunctions.afterListUserGroupsFunction = goAfterReqFunctions.afterListUserGroupsFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "listusergroups")) + } + if goAfterReqFunctions.afterListGroupsFunction != nil { + allAfterReqFunctions.afterListGroupsFunction = goAfterReqFunctions.afterListGroupsFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "listgroups")) + } + if goAfterReqFunctions.afterDeleteLeaderboardRecordFunction != nil { + allAfterReqFunctions.afterDeleteLeaderboardRecordFunction = goAfterReqFunctions.afterDeleteLeaderboardRecordFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "deleteleaderboardrecord")) + } + if goAfterReqFunctions.afterListLeaderboardRecordsFunction != nil { + allAfterReqFunctions.afterListLeaderboardRecordsFunction = goAfterReqFunctions.afterListLeaderboardRecordsFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "listleaderboardrecords")) + } + if goAfterReqFunctions.afterWriteLeaderboardRecordFunction != nil { + allAfterReqFunctions.afterWriteLeaderboardRecordFunction = goAfterReqFunctions.afterWriteLeaderboardRecordFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "writeleaderboardrecord")) + } + if goAfterReqFunctions.afterLinkCustomFunction != nil { + allAfterReqFunctions.afterLinkCustomFunction = goAfterReqFunctions.afterLinkCustomFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "linkcustom")) + } + if goAfterReqFunctions.afterLinkDeviceFunction != nil { + allAfterReqFunctions.afterLinkDeviceFunction = goAfterReqFunctions.afterLinkDeviceFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "linkdevice")) + } + if goAfterReqFunctions.afterLinkEmailFunction != nil { + allAfterReqFunctions.afterLinkEmailFunction = goAfterReqFunctions.afterLinkEmailFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "linkemail")) + } + if goAfterReqFunctions.afterLinkFacebookFunction != nil { + allAfterReqFunctions.afterLinkFacebookFunction = goAfterReqFunctions.afterLinkFacebookFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "linkfacebook")) + } + if goAfterReqFunctions.afterLinkGameCenterFunction != nil { + allAfterReqFunctions.afterLinkGameCenterFunction = goAfterReqFunctions.afterLinkGameCenterFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "linkgamecenter")) + } + if goAfterReqFunctions.afterLinkGoogleFunction != nil { + allAfterReqFunctions.afterLinkGoogleFunction = goAfterReqFunctions.afterLinkGoogleFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "linkgoogle")) + } + if goAfterReqFunctions.afterLinkSteamFunction != nil { + allAfterReqFunctions.afterLinkSteamFunction = goAfterReqFunctions.afterLinkSteamFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "linksteam")) + } + if goAfterReqFunctions.afterListMatchesFunction != nil { + allAfterReqFunctions.afterListMatchesFunction = goAfterReqFunctions.afterListMatchesFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "listmatches")) + } + if goAfterReqFunctions.afterListNotificationsFunction != nil { + allAfterReqFunctions.afterListNotificationsFunction = goAfterReqFunctions.afterListNotificationsFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "listnotifications")) + } + if goAfterReqFunctions.afterDeleteNotificationFunction != nil { + allAfterReqFunctions.afterDeleteNotificationFunction = goAfterReqFunctions.afterDeleteNotificationFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "deletenotification")) + } + if goAfterReqFunctions.afterListStorageObjectsFunction != nil { + allAfterReqFunctions.afterListStorageObjectsFunction = goAfterReqFunctions.afterListStorageObjectsFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "liststorageobjects")) + } + if goAfterReqFunctions.afterReadStorageObjectsFunction != nil { + allAfterReqFunctions.afterReadStorageObjectsFunction = goAfterReqFunctions.afterReadStorageObjectsFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "readstorageobjects")) + } + if goAfterReqFunctions.afterWriteStorageObjectsFunction != nil { + allAfterReqFunctions.afterWriteStorageObjectsFunction = goAfterReqFunctions.afterWriteStorageObjectsFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "writestorageobjects")) + } + if goAfterReqFunctions.afterDeleteStorageObjectsFunction != nil { + allAfterReqFunctions.afterDeleteStorageObjectsFunction = goAfterReqFunctions.afterDeleteStorageObjectsFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "deletestorageobjects")) + } + if goAfterReqFunctions.afterUnlinkCustomFunction != nil { + allAfterReqFunctions.afterUnlinkCustomFunction = goAfterReqFunctions.afterUnlinkCustomFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "unlinkcustom")) + } + if goAfterReqFunctions.afterUnlinkDeviceFunction != nil { + allAfterReqFunctions.afterUnlinkDeviceFunction = goAfterReqFunctions.afterUnlinkDeviceFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "unlinkdevice")) + } + if goAfterReqFunctions.afterUnlinkEmailFunction != nil { + allAfterReqFunctions.afterUnlinkEmailFunction = goAfterReqFunctions.afterUnlinkEmailFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "unlinkemail")) + } + if goAfterReqFunctions.afterUnlinkFacebookFunction != nil { + allAfterReqFunctions.afterUnlinkFacebookFunction = goAfterReqFunctions.afterUnlinkFacebookFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "unlinkfacebook")) + } + if goAfterReqFunctions.afterUnlinkGameCenterFunction != nil { + allAfterReqFunctions.afterUnlinkGameCenterFunction = goAfterReqFunctions.afterUnlinkGameCenterFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "unlinkgamecenter")) + } + if goAfterReqFunctions.afterUnlinkGoogleFunction != nil { + allAfterReqFunctions.afterUnlinkGoogleFunction = goAfterReqFunctions.afterUnlinkGoogleFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "unlinkgoogle")) + } + if goAfterReqFunctions.afterUnlinkSteamFunction != nil { + allAfterReqFunctions.afterUnlinkSteamFunction = goAfterReqFunctions.afterUnlinkSteamFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "unlinksteam")) + } + if goAfterReqFunctions.afterGetUsersFunction != nil { + allAfterReqFunctions.afterGetUsersFunction = goAfterReqFunctions.afterGetUsersFunction + startupLogger.Info("Registered Go runtime After function invocation", zap.String("id", "getusers")) } - retValue := l.Get(-1) - l.Pop(1) - if retValue.Type() == LTSentinel { - return nil, nil, 0 + var allMatchmakerMatchedFunction RuntimeMatchmakerMatchedFunction + switch { + case goMatchmakerMatchedFunction != nil: + allMatchmakerMatchedFunction = goMatchmakerMatchedFunction + startupLogger.Info("Registered Go runtime Matchmaker Matched function invocation") + case luaMatchmakerMatchedFunction != nil: + allMatchmakerMatchedFunction = luaMatchmakerMatchedFunction + startupLogger.Info("Registered Lua runtime Matchmaker Matched function invocation") } - // Unwind the stack up to and including our sentinel value, effectively discarding any other returned parameters. - for { - v := l.Get(-1) - l.Pop(1) - if v.Type() == LTSentinel { - break - } + // Lua matches are not registered the same, list only Go ones. + goMatchNames := goMatchNamesListFn() + for _, name := range goMatchNames { + startupLogger.Info("Registered Go runtime Match creation function invocation", zap.String("name", name)) } - return retValue, nil, 0 + return &Runtime{ + rpcFunctions: allRpcFunctions, + beforeRtFunctions: allBeforeRtFunctions, + afterRtFunctions: allAfterRtFunctions, + beforeReqFunctions: allBeforeReqFunctions, + afterReqFunctions: allAfterReqFunctions, + matchmakerMatchedFunction: allMatchmakerMatchedFunction, + }, nil +} + +func (r *Runtime) Rpc(id string) RuntimeRpcFunction { + return r.rpcFunctions[id] +} + +func (r *Runtime) BeforeRt(id string) RuntimeBeforeRtFunction { + return r.beforeRtFunctions[id] +} + +func (r *Runtime) AfterRt(id string) RuntimeAfterRtFunction { + return r.afterRtFunctions[id] +} + +func (r *Runtime) RuntimeBeforeGetAccount() RuntimeBeforeGetAccountFunction { + return r.beforeReqFunctions.beforeGetAccountFunction +} + +func (r *Runtime) AfterGetAccount() RuntimeAfterGetAccountFunction { + return r.afterReqFunctions.afterGetAccountFunction +} + +func (r *Runtime) BeforeUpdateAccount() RuntimeBeforeUpdateAccountFunction { + return r.beforeReqFunctions.beforeUpdateAccountFunction +} + +func (r *Runtime) AfterUpdateAccount() RuntimeAfterUpdateAccountFunction { + return r.afterReqFunctions.afterUpdateAccountFunction +} + +func (r *Runtime) BeforeAuthenticateCustom() RuntimeBeforeAuthenticateCustomFunction { + return r.beforeReqFunctions.beforeAuthenticateCustomFunction +} + +func (r *Runtime) AfterAuthenticateCustom() RuntimeAfterAuthenticateCustomFunction { + return r.afterReqFunctions.afterAuthenticateCustomFunction +} + +func (r *Runtime) BeforeAuthenticateDevice() RuntimeBeforeAuthenticateDeviceFunction { + return r.beforeReqFunctions.beforeAuthenticateDeviceFunction +} + +func (r *Runtime) AfterAuthenticateDevice() RuntimeAfterAuthenticateDeviceFunction { + return r.afterReqFunctions.afterAuthenticateDeviceFunction +} + +func (r *Runtime) BeforeAuthenticateEmail() RuntimeBeforeAuthenticateEmailFunction { + return r.beforeReqFunctions.beforeAuthenticateEmailFunction +} + +func (r *Runtime) AfterAuthenticateEmail() RuntimeAfterAuthenticateEmailFunction { + return r.afterReqFunctions.afterAuthenticateEmailFunction +} + +func (r *Runtime) BeforeAuthenticateFacebook() RuntimeBeforeAuthenticateFacebookFunction { + return r.beforeReqFunctions.beforeAuthenticateFacebookFunction +} + +func (r *Runtime) AfterAuthenticateFacebook() RuntimeAfterAuthenticateFacebookFunction { + return r.afterReqFunctions.afterAuthenticateFacebookFunction +} + +func (r *Runtime) BeforeAuthenticateGameCenter() RuntimeBeforeAuthenticateGameCenterFunction { + return r.beforeReqFunctions.beforeAuthenticateGameCenterFunction +} + +func (r *Runtime) AfterAuthenticateGameCenter() RuntimeAfterAuthenticateGameCenterFunction { + return r.afterReqFunctions.afterAuthenticateGameCenterFunction +} + +func (r *Runtime) BeforeAuthenticateGoogle() RuntimeBeforeAuthenticateGoogleFunction { + return r.beforeReqFunctions.beforeAuthenticateGoogleFunction +} + +func (r *Runtime) AfterAuthenticateGoogle() RuntimeAfterAuthenticateGoogleFunction { + return r.afterReqFunctions.afterAuthenticateGoogleFunction +} + +func (r *Runtime) BeforeAuthenticateSteam() RuntimeBeforeAuthenticateSteamFunction { + return r.beforeReqFunctions.beforeAuthenticateSteamFunction +} + +func (r *Runtime) AfterAuthenticateSteam() RuntimeAfterAuthenticateSteamFunction { + return r.afterReqFunctions.afterAuthenticateSteamFunction +} + +func (r *Runtime) BeforeListChannelMessages() RuntimeBeforeListChannelMessagesFunction { + return r.beforeReqFunctions.beforeListChannelMessagesFunction +} + +func (r *Runtime) AfterListChannelMessages() RuntimeAfterListChannelMessagesFunction { + return r.afterReqFunctions.afterListChannelMessagesFunction +} + +func (r *Runtime) BeforeListFriends() RuntimeBeforeListFriendsFunction { + return r.beforeReqFunctions.beforeListFriendsFunction +} + +func (r *Runtime) AfterListFriends() RuntimeAfterListFriendsFunction { + return r.afterReqFunctions.afterListFriendsFunction +} + +func (r *Runtime) BeforeAddFriends() RuntimeBeforeAddFriendsFunction { + return r.beforeReqFunctions.beforeAddFriendsFunction +} + +func (r *Runtime) AfterAddFriends() RuntimeAfterAddFriendsFunction { + return r.afterReqFunctions.afterAddFriendsFunction +} + +func (r *Runtime) BeforeDeleteFriends() RuntimeBeforeDeleteFriendsFunction { + return r.beforeReqFunctions.beforeDeleteFriendsFunction +} + +func (r *Runtime) AfterDeleteFriends() RuntimeAfterDeleteFriendsFunction { + return r.afterReqFunctions.afterDeleteFriendsFunction +} + +func (r *Runtime) BeforeBlockFriends() RuntimeBeforeBlockFriendsFunction { + return r.beforeReqFunctions.beforeBlockFriendsFunction +} + +func (r *Runtime) AfterBlockFriends() RuntimeAfterBlockFriendsFunction { + return r.afterReqFunctions.afterBlockFriendsFunction +} + +func (r *Runtime) BeforeImportFacebookFriends() RuntimeBeforeImportFacebookFriendsFunction { + return r.beforeReqFunctions.beforeImportFacebookFriendsFunction +} + +func (r *Runtime) AfterImportFacebookFriends() RuntimeAfterImportFacebookFriendsFunction { + return r.afterReqFunctions.afterImportFacebookFriendsFunction +} + +func (r *Runtime) BeforeCreateGroup() RuntimeBeforeCreateGroupFunction { + return r.beforeReqFunctions.beforeCreateGroupFunction +} + +func (r *Runtime) AfterCreateGroup() RuntimeAfterCreateGroupFunction { + return r.afterReqFunctions.afterCreateGroupFunction +} + +func (r *Runtime) BeforeUpdateGroup() RuntimeBeforeUpdateGroupFunction { + return r.beforeReqFunctions.beforeUpdateGroupFunction +} + +func (r *Runtime) AfterUpdateGroup() RuntimeAfterUpdateGroupFunction { + return r.afterReqFunctions.afterUpdateGroupFunction +} + +func (r *Runtime) BeforeDeleteGroup() RuntimeBeforeDeleteGroupFunction { + return r.beforeReqFunctions.beforeDeleteGroupFunction +} + +func (r *Runtime) AfterDeleteGroup() RuntimeAfterDeleteGroupFunction { + return r.afterReqFunctions.afterDeleteGroupFunction +} + +func (r *Runtime) BeforeJoinGroup() RuntimeBeforeJoinGroupFunction { + return r.beforeReqFunctions.beforeJoinGroupFunction +} + +func (r *Runtime) AfterJoinGroup() RuntimeAfterJoinGroupFunction { + return r.afterReqFunctions.afterJoinGroupFunction +} + +func (r *Runtime) BeforeLeaveGroup() RuntimeBeforeLeaveGroupFunction { + return r.beforeReqFunctions.beforeLeaveGroupFunction +} + +func (r *Runtime) AfterLeaveGroup() RuntimeAfterLeaveGroupFunction { + return r.afterReqFunctions.afterLeaveGroupFunction +} + +func (r *Runtime) BeforeAddGroupUsers() RuntimeBeforeAddGroupUsersFunction { + return r.beforeReqFunctions.beforeAddGroupUsersFunction +} + +func (r *Runtime) AfterAddGroupUsers() RuntimeAfterAddGroupUsersFunction { + return r.afterReqFunctions.afterAddGroupUsersFunction +} + +func (r *Runtime) BeforeKickGroupUsers() RuntimeBeforeKickGroupUsersFunction { + return r.beforeReqFunctions.beforeKickGroupUsersFunction +} + +func (r *Runtime) AfterKickGroupUsers() RuntimeAfterKickGroupUsersFunction { + return r.afterReqFunctions.afterKickGroupUsersFunction +} + +func (r *Runtime) BeforePromoteGroupUsers() RuntimeBeforePromoteGroupUsersFunction { + return r.beforeReqFunctions.beforePromoteGroupUsersFunction +} + +func (r *Runtime) AfterPromoteGroupUsers() RuntimeAfterPromoteGroupUsersFunction { + return r.afterReqFunctions.afterPromoteGroupUsersFunction +} + +func (r *Runtime) BeforeListGroupUsers() RuntimeBeforeListGroupUsersFunction { + return r.beforeReqFunctions.beforeListGroupUsersFunction +} + +func (r *Runtime) AfterListGroupUsers() RuntimeAfterListGroupUsersFunction { + return r.afterReqFunctions.afterListGroupUsersFunction +} + +func (r *Runtime) BeforeListUserGroups() RuntimeBeforeListUserGroupsFunction { + return r.beforeReqFunctions.beforeListUserGroupsFunction +} + +func (r *Runtime) AfterListUserGroups() RuntimeAfterListUserGroupsFunction { + return r.afterReqFunctions.afterListUserGroupsFunction +} + +func (r *Runtime) BeforeListGroups() RuntimeBeforeListGroupsFunction { + return r.beforeReqFunctions.beforeListGroupsFunction +} + +func (r *Runtime) AfterListGroups() RuntimeAfterListGroupsFunction { + return r.afterReqFunctions.afterListGroupsFunction +} + +func (r *Runtime) BeforeDeleteLeaderboardRecord() RuntimeBeforeDeleteLeaderboardRecordFunction { + return r.beforeReqFunctions.beforeDeleteLeaderboardRecordFunction +} + +func (r *Runtime) AfterDeleteLeaderboardRecord() RuntimeAfterDeleteLeaderboardRecordFunction { + return r.afterReqFunctions.afterDeleteLeaderboardRecordFunction +} + +func (r *Runtime) BeforeListLeaderboardRecords() RuntimeBeforeListLeaderboardRecordsFunction { + return r.beforeReqFunctions.beforeListLeaderboardRecordsFunction +} + +func (r *Runtime) AfterListLeaderboardRecords() RuntimeAfterListLeaderboardRecordsFunction { + return r.afterReqFunctions.afterListLeaderboardRecordsFunction +} + +func (r *Runtime) BeforeWriteLeaderboardRecord() RuntimeBeforeWriteLeaderboardRecordFunction { + return r.beforeReqFunctions.beforeWriteLeaderboardRecordFunction +} + +func (r *Runtime) AfterWriteLeaderboardRecord() RuntimeAfterWriteLeaderboardRecordFunction { + return r.afterReqFunctions.afterWriteLeaderboardRecordFunction +} + +func (r *Runtime) BeforeLinkCustom() RuntimeBeforeLinkCustomFunction { + return r.beforeReqFunctions.beforeLinkCustomFunction +} + +func (r *Runtime) AfterLinkCustom() RuntimeAfterLinkCustomFunction { + return r.afterReqFunctions.afterLinkCustomFunction +} + +func (r *Runtime) BeforeLinkDevice() RuntimeBeforeLinkDeviceFunction { + return r.beforeReqFunctions.beforeLinkDeviceFunction +} + +func (r *Runtime) AfterLinkDevice() RuntimeAfterLinkDeviceFunction { + return r.afterReqFunctions.afterLinkDeviceFunction +} + +func (r *Runtime) BeforeLinkEmail() RuntimeBeforeLinkEmailFunction { + return r.beforeReqFunctions.beforeLinkEmailFunction +} + +func (r *Runtime) AfterLinkEmail() RuntimeAfterLinkEmailFunction { + return r.afterReqFunctions.afterLinkEmailFunction +} + +func (r *Runtime) BeforeLinkFacebook() RuntimeBeforeLinkFacebookFunction { + return r.beforeReqFunctions.beforeLinkFacebookFunction +} + +func (r *Runtime) AfterLinkFacebook() RuntimeAfterLinkFacebookFunction { + return r.afterReqFunctions.afterLinkFacebookFunction +} + +func (r *Runtime) BeforeLinkGameCenter() RuntimeBeforeLinkGameCenterFunction { + return r.beforeReqFunctions.beforeLinkGameCenterFunction +} + +func (r *Runtime) AfterLinkGameCenter() RuntimeAfterLinkGameCenterFunction { + return r.afterReqFunctions.afterLinkGameCenterFunction +} + +func (r *Runtime) BeforeLinkGoogle() RuntimeBeforeLinkGoogleFunction { + return r.beforeReqFunctions.beforeLinkGoogleFunction +} + +func (r *Runtime) AfterLinkGoogle() RuntimeAfterLinkGoogleFunction { + return r.afterReqFunctions.afterLinkGoogleFunction +} + +func (r *Runtime) BeforeLinkSteam() RuntimeBeforeLinkSteamFunction { + return r.beforeReqFunctions.beforeLinkSteamFunction +} + +func (r *Runtime) AfterLinkSteam() RuntimeAfterLinkSteamFunction { + return r.afterReqFunctions.afterLinkSteamFunction +} + +func (r *Runtime) BeforeListMatches() RuntimeBeforeListMatchesFunction { + return r.beforeReqFunctions.beforeListMatchesFunction +} + +func (r *Runtime) AfterListMatches() RuntimeAfterListMatchesFunction { + return r.afterReqFunctions.afterListMatchesFunction +} + +func (r *Runtime) BeforeListNotifications() RuntimeBeforeListNotificationsFunction { + return r.beforeReqFunctions.beforeListNotificationsFunction +} + +func (r *Runtime) AfterListNotifications() RuntimeAfterListNotificationsFunction { + return r.afterReqFunctions.afterListNotificationsFunction +} + +func (r *Runtime) BeforeDeleteNotification() RuntimeBeforeDeleteNotificationFunction { + return r.beforeReqFunctions.beforeDeleteNotificationFunction +} + +func (r *Runtime) AfterDeleteNotification() RuntimeAfterDeleteNotificationFunction { + return r.afterReqFunctions.afterDeleteNotificationFunction +} + +func (r *Runtime) BeforeListStorageObjects() RuntimeBeforeListStorageObjectsFunction { + return r.beforeReqFunctions.beforeListStorageObjectsFunction +} + +func (r *Runtime) AfterListStorageObjects() RuntimeAfterListStorageObjectsFunction { + return r.afterReqFunctions.afterListStorageObjectsFunction +} + +func (r *Runtime) BeforeReadStorageObjects() RuntimeBeforeReadStorageObjectsFunction { + return r.beforeReqFunctions.beforeReadStorageObjectsFunction +} + +func (r *Runtime) AfterReadStorageObjects() RuntimeAfterReadStorageObjectsFunction { + return r.afterReqFunctions.afterReadStorageObjectsFunction +} + +func (r *Runtime) BeforeWriteStorageObjects() RuntimeBeforeWriteStorageObjectsFunction { + return r.beforeReqFunctions.beforeWriteStorageObjectsFunction +} + +func (r *Runtime) AfterWriteStorageObjects() RuntimeAfterWriteStorageObjectsFunction { + return r.afterReqFunctions.afterWriteStorageObjectsFunction +} + +func (r *Runtime) BeforeDeleteStorageObjects() RuntimeBeforeDeleteStorageObjectsFunction { + return r.beforeReqFunctions.beforeDeleteStorageObjectsFunction +} + +func (r *Runtime) AfterDeleteStorageObjects() RuntimeAfterDeleteStorageObjectsFunction { + return r.afterReqFunctions.afterDeleteStorageObjectsFunction +} + +func (r *Runtime) BeforeUnlinkCustom() RuntimeBeforeUnlinkCustomFunction { + return r.beforeReqFunctions.beforeUnlinkCustomFunction +} + +func (r *Runtime) AfterUnlinkCustom() RuntimeAfterUnlinkCustomFunction { + return r.afterReqFunctions.afterUnlinkCustomFunction +} + +func (r *Runtime) BeforeUnlinkDevice() RuntimeBeforeUnlinkDeviceFunction { + return r.beforeReqFunctions.beforeUnlinkDeviceFunction +} + +func (r *Runtime) AfterUnlinkDevice() RuntimeAfterUnlinkDeviceFunction { + return r.afterReqFunctions.afterUnlinkDeviceFunction +} + +func (r *Runtime) BeforeUnlinkEmail() RuntimeBeforeUnlinkEmailFunction { + return r.beforeReqFunctions.beforeUnlinkEmailFunction +} + +func (r *Runtime) AfterUnlinkEmail() RuntimeAfterUnlinkEmailFunction { + return r.afterReqFunctions.afterUnlinkEmailFunction +} + +func (r *Runtime) BeforeUnlinkFacebook() RuntimeBeforeUnlinkFacebookFunction { + return r.beforeReqFunctions.beforeUnlinkFacebookFunction +} + +func (r *Runtime) AfterUnlinkFacebook() RuntimeAfterUnlinkFacebookFunction { + return r.afterReqFunctions.afterUnlinkFacebookFunction +} + +func (r *Runtime) BeforeUnlinkGameCenter() RuntimeBeforeUnlinkGameCenterFunction { + return r.beforeReqFunctions.beforeUnlinkGameCenterFunction +} + +func (r *Runtime) AfterUnlinkGameCenter() RuntimeAfterUnlinkGameCenterFunction { + return r.afterReqFunctions.afterUnlinkGameCenterFunction +} + +func (r *Runtime) BeforeUnlinkGoogle() RuntimeBeforeUnlinkGoogleFunction { + return r.beforeReqFunctions.beforeUnlinkGoogleFunction +} + +func (r *Runtime) AfterUnlinkGoogle() RuntimeAfterUnlinkGoogleFunction { + return r.afterReqFunctions.afterUnlinkGoogleFunction +} + +func (r *Runtime) BeforeUnlinkSteam() RuntimeBeforeUnlinkSteamFunction { + return r.beforeReqFunctions.beforeUnlinkSteamFunction +} + +func (r *Runtime) AfterUnlinkSteam() RuntimeAfterUnlinkSteamFunction { + return r.afterReqFunctions.afterUnlinkSteamFunction +} + +func (r *Runtime) BeforeGetUsers() RuntimeBeforeGetUsersFunction { + return r.beforeReqFunctions.beforeGetUsersFunction +} + +func (r *Runtime) AfterGetUsers() RuntimeAfterGetUsersFunction { + return r.afterReqFunctions.afterGetUsersFunction } -func (r *Runtime) Stop() { - // Not necessarily required as it only does OS temp files cleanup, which we don't expose in the runtime. - r.vm.Close() +func (r *Runtime) MatchmakerMatched() RuntimeMatchmakerMatchedFunction { + return r.matchmakerMatchedFunction } diff --git a/server/runtime_go.go b/server/runtime_go.go new file mode 100644 index 0000000000000000000000000000000000000000..6013fb3884afb147aa2b8be7f98141f3682b19b2 --- /dev/null +++ b/server/runtime_go.go @@ -0,0 +1,1062 @@ +// Copyright 2018 The Nakama Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import ( + "context" + "database/sql" + "errors" + "github.com/gofrs/uuid" + "github.com/golang/protobuf/ptypes/empty" + "github.com/heroiclabs/nakama/api" + "github.com/heroiclabs/nakama/rtapi" + "github.com/heroiclabs/nakama/runtime" + "github.com/heroiclabs/nakama/social" + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "log" + "path/filepath" + "plugin" + "strings" + "sync" +) + +// No need for a stateful RuntimeProviderGo here. + +type RuntimeGoInitialiser struct { + logger *log.Logger + db *sql.DB + env map[string]string + nk runtime.NakamaModule + + rpc map[string]RuntimeRpcFunction + beforeRt map[string]RuntimeBeforeRtFunction + afterRt map[string]RuntimeAfterRtFunction + beforeReq *RuntimeBeforeReqFunctions + afterReq *RuntimeAfterReqFunctions + matchmakerMatched RuntimeMatchmakerMatchedFunction + + match map[string]func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule) (runtime.Match, error) + matchLock *sync.RWMutex +} + +func (ri *RuntimeGoInitialiser) RegisterRpc(id string, fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error, int)) error { + id = strings.ToLower(id) + ri.rpc[id] = func(queryParams map[string][]string, userID, username string, expiry int64, sessionID, clientIP, clientPort, payload string) (string, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeRPC, queryParams, expiry, userID, username, sessionID, clientIP, clientPort) + result, fnErr, code := fn(ctx, ri.logger, ri.db, ri.nk, payload) + return result, fnErr, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeRt(id string, fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, envelope *rtapi.Envelope) (*rtapi.Envelope, error)) error { + id = strings.ToLower(RTAPI_PREFIX + id) + ri.beforeRt[id] = func(logger *zap.Logger, userID, username string, expiry int64, sessionID, clientIP, clientPort string, envelope *rtapi.Envelope) (*rtapi.Envelope, error) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, sessionID, clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, envelope) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterRt(id string, fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, envelope *rtapi.Envelope) error) error { + id = strings.ToLower(RTAPI_PREFIX + id) + ri.afterRt[id] = func(logger *zap.Logger, userID, username string, expiry int64, sessionID, clientIP, clientPort string, envelope *rtapi.Envelope) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, sessionID, clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, envelope) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeGetAccount(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *empty.Empty) (*empty.Empty, error, int)) error { + ri.beforeReq.beforeGetAccountFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) (*empty.Empty, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterGetAccount(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *api.Account) error) error { + ri.afterReq.afterGetAccountFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.Account) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeUpdateAccount(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.UpdateAccountRequest) (*api.UpdateAccountRequest, error, int)) error { + ri.beforeReq.beforeUpdateAccountFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.UpdateAccountRequest) (*api.UpdateAccountRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterUpdateAccount(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterUpdateAccountFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeAuthenticateCustom(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AuthenticateCustomRequest) (*api.AuthenticateCustomRequest, error, int)) error { + ri.beforeReq.beforeAuthenticateCustomFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AuthenticateCustomRequest) (*api.AuthenticateCustomRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterAuthenticateCustom(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *api.Session) error) error { + ri.afterReq.afterAuthenticateCustomFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.Session) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeAuthenticateDevice(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AuthenticateDeviceRequest) (*api.AuthenticateDeviceRequest, error, int)) error { + ri.beforeReq.beforeAuthenticateDeviceFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AuthenticateDeviceRequest) (*api.AuthenticateDeviceRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterAuthenticateDevice(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *api.Session) error) error { + ri.afterReq.afterAuthenticateDeviceFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.Session) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeAuthenticateEmail(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AuthenticateEmailRequest) (*api.AuthenticateEmailRequest, error, int)) error { + ri.beforeReq.beforeAuthenticateEmailFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AuthenticateEmailRequest) (*api.AuthenticateEmailRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterAuthenticateEmail(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *api.Session) error) error { + ri.afterReq.afterAuthenticateEmailFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.Session) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeAuthenticateFacebook(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AuthenticateFacebookRequest) (*api.AuthenticateFacebookRequest, error, int)) error { + ri.beforeReq.beforeAuthenticateFacebookFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AuthenticateFacebookRequest) (*api.AuthenticateFacebookRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterAuthenticateFacebook(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *api.Session) error) error { + ri.afterReq.afterAuthenticateFacebookFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.Session) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeAuthenticateGameCenter(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AuthenticateGameCenterRequest) (*api.AuthenticateGameCenterRequest, error, int)) error { + ri.beforeReq.beforeAuthenticateGameCenterFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AuthenticateGameCenterRequest) (*api.AuthenticateGameCenterRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterAuthenticateGameCenter(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *api.Session) error) error { + ri.afterReq.afterAuthenticateGameCenterFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.Session) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeAuthenticateGoogle(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AuthenticateGoogleRequest) (*api.AuthenticateGoogleRequest, error, int)) error { + ri.beforeReq.beforeAuthenticateGoogleFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AuthenticateGoogleRequest) (*api.AuthenticateGoogleRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterAuthenticateGoogle(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *api.Session) error) error { + ri.afterReq.afterAuthenticateGoogleFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.Session) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeAuthenticateSteam(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AuthenticateSteamRequest) (*api.AuthenticateSteamRequest, error, int)) error { + ri.beforeReq.beforeAuthenticateSteamFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AuthenticateSteamRequest) (*api.AuthenticateSteamRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterAuthenticateSteam(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *api.Session) error) error { + ri.afterReq.afterAuthenticateSteamFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.Session) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeListChannelMessages(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.ListChannelMessagesRequest) (*api.ListChannelMessagesRequest, error, int)) error { + ri.beforeReq.beforeListChannelMessagesFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ListChannelMessagesRequest) (*api.ListChannelMessagesRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterListChannelMessages(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *api.ChannelMessageList) error) error { + ri.afterReq.afterListChannelMessagesFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ChannelMessageList) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeListFriends(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *empty.Empty) (*empty.Empty, error, int)) error { + ri.beforeReq.beforeListFriendsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) (*empty.Empty, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterListFriends(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *api.Friends) error) error { + ri.afterReq.afterListFriendsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.Friends) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeAddFriends(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AddFriendsRequest) (*api.AddFriendsRequest, error, int)) error { + ri.beforeReq.beforeAddFriendsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AddFriendsRequest) (*api.AddFriendsRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterAddFriends(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterAddFriendsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeDeleteFriends(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.DeleteFriendsRequest) (*api.DeleteFriendsRequest, error, int)) error { + ri.beforeReq.beforeDeleteFriendsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.DeleteFriendsRequest) (*api.DeleteFriendsRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterDeleteFriends(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterDeleteFriendsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeBlockFriends(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.BlockFriendsRequest) (*api.BlockFriendsRequest, error, int)) error { + ri.beforeReq.beforeBlockFriendsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.BlockFriendsRequest) (*api.BlockFriendsRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterBlockFriends(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterBlockFriendsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeImportFacebookFriends(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.ImportFacebookFriendsRequest) (*api.ImportFacebookFriendsRequest, error, int)) error { + ri.beforeReq.beforeImportFacebookFriendsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ImportFacebookFriendsRequest) (*api.ImportFacebookFriendsRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterImportFacebookFriends(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterImportFacebookFriendsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeCreateGroup(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.CreateGroupRequest) (*api.CreateGroupRequest, error, int)) error { + ri.beforeReq.beforeCreateGroupFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.CreateGroupRequest) (*api.CreateGroupRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterCreateGroup(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *api.Group) error) error { + ri.afterReq.afterCreateGroupFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.Group) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeUpdateGroup(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.UpdateGroupRequest) (*api.UpdateGroupRequest, error, int)) error { + ri.beforeReq.beforeUpdateGroupFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.UpdateGroupRequest) (*api.UpdateGroupRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterUpdateGroup(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterUpdateGroupFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeDeleteGroup(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.DeleteGroupRequest) (*api.DeleteGroupRequest, error, int)) error { + ri.beforeReq.beforeDeleteGroupFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.DeleteGroupRequest) (*api.DeleteGroupRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterDeleteGroup(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterDeleteGroupFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeJoinGroup(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.JoinGroupRequest) (*api.JoinGroupRequest, error, int)) error { + ri.beforeReq.beforeJoinGroupFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.JoinGroupRequest) (*api.JoinGroupRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterJoinGroup(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterJoinGroupFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeLeaveGroup(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.LeaveGroupRequest) (*api.LeaveGroupRequest, error, int)) error { + ri.beforeReq.beforeLeaveGroupFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.LeaveGroupRequest) (*api.LeaveGroupRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterLeaveGroup(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterLeaveGroupFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeAddGroupUsers(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AddGroupUsersRequest) (*api.AddGroupUsersRequest, error, int)) error { + ri.beforeReq.beforeAddGroupUsersFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AddGroupUsersRequest) (*api.AddGroupUsersRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterAddGroupUsers(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterAddGroupUsersFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeKickGroupUsers(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.KickGroupUsersRequest) (*api.KickGroupUsersRequest, error, int)) error { + ri.beforeReq.beforeKickGroupUsersFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.KickGroupUsersRequest) (*api.KickGroupUsersRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterKickGroupUsers(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterKickGroupUsersFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforePromoteGroupUsers(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.PromoteGroupUsersRequest) (*api.PromoteGroupUsersRequest, error, int)) error { + ri.beforeReq.beforePromoteGroupUsersFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.PromoteGroupUsersRequest) (*api.PromoteGroupUsersRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterPromoteGroupUsers(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterPromoteGroupUsersFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeListGroupUsers(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.ListGroupUsersRequest) (*api.ListGroupUsersRequest, error, int)) error { + ri.beforeReq.beforeListGroupUsersFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ListGroupUsersRequest) (*api.ListGroupUsersRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterListGroupUsers(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *api.GroupUserList) error) error { + ri.afterReq.afterListGroupUsersFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.GroupUserList) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeListUserGroups(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.ListUserGroupsRequest) (*api.ListUserGroupsRequest, error, int)) error { + ri.beforeReq.beforeListUserGroupsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ListUserGroupsRequest) (*api.ListUserGroupsRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterListUserGroups(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *api.UserGroupList) error) error { + ri.afterReq.afterListUserGroupsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.UserGroupList) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeListGroups(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.ListGroupsRequest) (*api.ListGroupsRequest, error, int)) error { + ri.beforeReq.beforeListGroupsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ListGroupsRequest) (*api.ListGroupsRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterListGroups(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *api.GroupList) error) error { + ri.afterReq.afterListGroupsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.GroupList) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeDeleteLeaderboardRecord(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.DeleteLeaderboardRecordRequest) (*api.DeleteLeaderboardRecordRequest, error, int)) error { + ri.beforeReq.beforeDeleteLeaderboardRecordFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.DeleteLeaderboardRecordRequest) (*api.DeleteLeaderboardRecordRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterDeleteLeaderboardRecord(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterDeleteLeaderboardRecordFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeListLeaderboardRecords(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.ListLeaderboardRecordsRequest) (*api.ListLeaderboardRecordsRequest, error, int)) error { + ri.beforeReq.beforeListLeaderboardRecordsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ListLeaderboardRecordsRequest) (*api.ListLeaderboardRecordsRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterListLeaderboardRecords(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *api.LeaderboardRecordList) error) error { + ri.afterReq.afterListLeaderboardRecordsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.LeaderboardRecordList) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeWriteLeaderboardRecord(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.WriteLeaderboardRecordRequest) (*api.WriteLeaderboardRecordRequest, error, int)) error { + ri.beforeReq.beforeWriteLeaderboardRecordFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.WriteLeaderboardRecordRequest) (*api.WriteLeaderboardRecordRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterWriteLeaderboardRecord(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *api.LeaderboardRecord) error) error { + ri.afterReq.afterWriteLeaderboardRecordFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.LeaderboardRecord) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeLinkCustom(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AccountCustom) (*api.AccountCustom, error, int)) error { + ri.beforeReq.beforeLinkCustomFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountCustom) (*api.AccountCustom, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterLinkCustom(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterLinkCustomFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeLinkDevice(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AccountDevice) (*api.AccountDevice, error, int)) error { + ri.beforeReq.beforeLinkDeviceFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountDevice) (*api.AccountDevice, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterLinkDevice(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterLinkDeviceFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeLinkEmail(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AccountEmail) (*api.AccountEmail, error, int)) error { + ri.beforeReq.beforeLinkEmailFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountEmail) (*api.AccountEmail, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterLinkEmail(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterLinkEmailFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeLinkFacebook(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.LinkFacebookRequest) (*api.LinkFacebookRequest, error, int)) error { + ri.beforeReq.beforeLinkFacebookFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.LinkFacebookRequest) (*api.LinkFacebookRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterLinkFacebook(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterLinkFacebookFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeLinkGameCenter(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AccountGameCenter) (*api.AccountGameCenter, error, int)) error { + ri.beforeReq.beforeLinkGameCenterFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountGameCenter) (*api.AccountGameCenter, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterLinkGameCenter(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterLinkGameCenterFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeLinkGoogle(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AccountGoogle) (*api.AccountGoogle, error, int)) error { + ri.beforeReq.beforeLinkGoogleFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountGoogle) (*api.AccountGoogle, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterLinkGoogle(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterLinkGoogleFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeLinkSteam(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AccountSteam) (*api.AccountSteam, error, int)) error { + ri.beforeReq.beforeLinkSteamFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountSteam) (*api.AccountSteam, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterLinkSteam(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterLinkSteamFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeListMatches(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.ListMatchesRequest) (*api.ListMatchesRequest, error, int)) error { + ri.beforeReq.beforeListMatchesFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ListMatchesRequest) (*api.ListMatchesRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterListMatches(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *api.MatchList) error) error { + ri.afterReq.afterListMatchesFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.MatchList) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeListNotifications(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.ListNotificationsRequest) (*api.ListNotificationsRequest, error, int)) error { + ri.beforeReq.beforeListNotificationsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ListNotificationsRequest) (*api.ListNotificationsRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterListNotifications(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *api.NotificationList) error) error { + ri.afterReq.afterListNotificationsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.NotificationList) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeDeleteNotification(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.DeleteNotificationsRequest) (*api.DeleteNotificationsRequest, error, int)) error { + ri.beforeReq.beforeDeleteNotificationFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.DeleteNotificationsRequest) (*api.DeleteNotificationsRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterDeleteNotification(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterDeleteNotificationFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeListStorageObjects(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.ListStorageObjectsRequest) (*api.ListStorageObjectsRequest, error, int)) error { + ri.beforeReq.beforeListStorageObjectsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ListStorageObjectsRequest) (*api.ListStorageObjectsRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterListStorageObjects(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *api.StorageObjectList) error) error { + ri.afterReq.afterListStorageObjectsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.StorageObjectList) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeReadStorageObjects(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.ReadStorageObjectsRequest) (*api.ReadStorageObjectsRequest, error, int)) error { + ri.beforeReq.beforeReadStorageObjectsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ReadStorageObjectsRequest) (*api.ReadStorageObjectsRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterReadStorageObjects(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *api.StorageObjects) error) error { + ri.afterReq.afterReadStorageObjectsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.StorageObjects) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeWriteStorageObjects(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.WriteStorageObjectsRequest) (*api.WriteStorageObjectsRequest, error, int)) error { + ri.beforeReq.beforeWriteStorageObjectsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.WriteStorageObjectsRequest) (*api.WriteStorageObjectsRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterWriteStorageObjects(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *api.StorageObjectAcks) error) error { + ri.afterReq.afterWriteStorageObjectsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.StorageObjectAcks) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeDeleteStorageObjects(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.DeleteStorageObjectsRequest) (*api.DeleteStorageObjectsRequest, error, int)) error { + ri.beforeReq.beforeDeleteStorageObjectsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.DeleteStorageObjectsRequest) (*api.DeleteStorageObjectsRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterDeleteStorageObjects(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterDeleteStorageObjectsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeUnlinkCustom(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AccountCustom) (*api.AccountCustom, error, int)) error { + ri.beforeReq.beforeUnlinkCustomFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountCustom) (*api.AccountCustom, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterUnlinkCustom(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterUnlinkCustomFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeUnlinkDevice(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AccountDevice) (*api.AccountDevice, error, int)) error { + ri.beforeReq.beforeUnlinkDeviceFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountDevice) (*api.AccountDevice, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterUnlinkDevice(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterUnlinkDeviceFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeUnlinkEmail(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AccountEmail) (*api.AccountEmail, error, int)) error { + ri.beforeReq.beforeUnlinkEmailFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountEmail) (*api.AccountEmail, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterUnlinkEmail(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterUnlinkEmailFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeUnlinkFacebook(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AccountFacebook) (*api.AccountFacebook, error, int)) error { + ri.beforeReq.beforeUnlinkFacebookFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountFacebook) (*api.AccountFacebook, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterUnlinkFacebook(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterUnlinkFacebookFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeUnlinkGameCenter(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AccountGameCenter) (*api.AccountGameCenter, error, int)) error { + ri.beforeReq.beforeUnlinkGameCenterFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountGameCenter) (*api.AccountGameCenter, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterUnlinkGameCenter(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterUnlinkGameCenterFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeUnlinkGoogle(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AccountGoogle) (*api.AccountGoogle, error, int)) error { + ri.beforeReq.beforeUnlinkGoogleFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountGoogle) (*api.AccountGoogle, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterUnlinkGoogle(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterUnlinkGoogleFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeUnlinkSteam(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AccountSteam) (*api.AccountSteam, error, int)) error { + ri.beforeReq.beforeUnlinkSteamFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountSteam) (*api.AccountSteam, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterUnlinkSteam(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *empty.Empty) error) error { + ri.afterReq.afterUnlinkSteamFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterBeforeGetUsers(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.GetUsersRequest) (*api.GetUsersRequest, error, int)) error { + ri.beforeReq.beforeGetUsersFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.GetUsersRequest) (*api.GetUsersRequest, error, codes.Code) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeBefore, nil, expiry, userID, username, "", clientIP, clientPort) + result, err, code := fn(ctx, ri.logger, ri.db, ri.nk, in) + return result, err, codes.Code(code) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterAfterGetUsers(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, out *api.Users) error) error { + ri.afterReq.afterGetUsersFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.Users) error { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeAfter, nil, expiry, userID, username, "", clientIP, clientPort) + return fn(ctx, ri.logger, ri.db, ri.nk, in) + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterMatchmakerMatched(fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, entries []runtime.MatchmakerEntry) (string, error)) error { + ri.matchmakerMatched = func(entries []*MatchmakerEntry) (string, bool, error) { + ctx := NewRuntimeGoContext(ri.env, RuntimeExecutionModeMatchmaker, nil, 0, "", "", "", "", "") + runtimeEntries := make([]runtime.MatchmakerEntry, len(entries)) + for i, entry := range entries { + runtimeEntries[i] = runtime.MatchmakerEntry(entry) + } + matchID, err := fn(ctx, ri.logger, ri.db, ri.nk, runtimeEntries) + if err != nil { + return "", false, err + } + return matchID, matchID != "", nil + } + return nil +} + +func (ri *RuntimeGoInitialiser) RegisterMatch(name string, fn func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule) (runtime.Match, error)) error { + ri.matchLock.Lock() + ri.match[name] = fn + ri.matchLock.Unlock() + return nil +} + +func NewRuntimeProviderGo(logger, startupLogger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, leaderboardCache LeaderboardCache, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter, rootPath string, paths []string) ([]string, map[string]RuntimeRpcFunction, map[string]RuntimeBeforeRtFunction, map[string]RuntimeAfterRtFunction, *RuntimeBeforeReqFunctions, *RuntimeAfterReqFunctions, RuntimeMatchmakerMatchedFunction, RuntimeMatchCreateFunction, func(RuntimeMatchCreateFunction), func() []string, error) { + stdLogger := zap.NewStdLog(logger) + env := config.GetRuntime().Environment + nk := NewRuntimeGoNakamaModule(logger, db, config, socialClient, leaderboardCache, sessionRegistry, matchRegistry, tracker, router) + + match := make(map[string]func(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule) (runtime.Match, error), 0) + matchLock := &sync.RWMutex{} + matchCreateFn := func(logger *zap.Logger, id uuid.UUID, node string, name string, labelUpdateFn func(string)) (RuntimeMatchCore, error) { + matchLock.RLock() + fn, ok := match[name] + matchLock.RUnlock() + if !ok { + // Not a Go match. + return nil, nil + } + + ctx := NewRuntimeGoContext(env, RuntimeExecutionModeMatchCreate, nil, 0, "", "", "", "", "") + match, err := fn(ctx, stdLogger, db, nk) + if err != nil { + return nil, err + } + + return NewRuntimeGoMatchCore(logger, matchRegistry, tracker, router, id, node, labelUpdateFn, stdLogger, db, env, nk, match) + } + nk.SetMatchCreateFn(matchCreateFn) + matchNamesListFn := func() []string { + matchLock.RLock() + matchNames := make([]string, 0, len(match)) + for name, _ := range match { + matchNames = append(matchNames, name) + } + matchLock.RUnlock() + return matchNames + } + + initializer := &RuntimeGoInitialiser{ + logger: stdLogger, + db: db, + env: env, + nk: nk, + + rpc: make(map[string]RuntimeRpcFunction, 0), + + beforeRt: make(map[string]RuntimeBeforeRtFunction, 0), + afterRt: make(map[string]RuntimeAfterRtFunction, 0), + + beforeReq: &RuntimeBeforeReqFunctions{}, + afterReq: &RuntimeAfterReqFunctions{}, + + match: match, + matchLock: matchLock, + } + + modulePaths := make([]string, 0) + for _, path := range paths { + if strings.ToLower(filepath.Ext(path)) != ".so" { + continue + } + + relPath, _ := filepath.Rel(rootPath, path) + name := strings.TrimSuffix(relPath, filepath.Ext(relPath)) + + // Open the plugin. + p, err := plugin.Open(path) + if err != nil { + startupLogger.Error("Could not open Go module", zap.String("path", path), zap.Error(err)) + return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err + } + + // Look up the required initialisation function. + f, err := p.Lookup("InitModule") + if err != nil { + startupLogger.Fatal("Error looking up InitModule function in Go module", zap.String("name", name)) + return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err + } + + // Ensure the function has the correct signature. + fn, ok := f.(func(context.Context, *log.Logger, *sql.DB, runtime.NakamaModule, runtime.Initializer)) + if !ok { + startupLogger.Fatal("Error reading InitModule function in Go module", zap.String("name", name)) + return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, errors.New("error reading InitModule function in Go module") + } + + // Run the initialisation. + fn(context.Background(), stdLogger, db, nk, initializer) + modulePaths = append(modulePaths, relPath) + } + + return modulePaths, initializer.rpc, initializer.beforeRt, initializer.afterRt, initializer.beforeReq, initializer.afterReq, initializer.matchmakerMatched, matchCreateFn, nk.SetMatchCreateFn, matchNamesListFn, nil +} diff --git a/server/runtime_go_context.go b/server/runtime_go_context.go new file mode 100644 index 0000000000000000000000000000000000000000..2d1b1abf5835a4575a74144375c234b1d0e1b505 --- /dev/null +++ b/server/runtime_go_context.go @@ -0,0 +1,46 @@ +// Copyright 2018 The Nakama Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import ( + "context" + "github.com/heroiclabs/nakama/runtime" +) + +func NewRuntimeGoContext(env map[string]string, mode RuntimeExecutionMode, queryParams map[string][]string, sessionExpiry int64, userID, username, sessionID, clientIP, clientPort string) context.Context { + ctx := context.Background() + + ctx = context.WithValue(ctx, runtime.RUNTIME_CTX_ENV, env) + ctx = context.WithValue(ctx, runtime.RUNTIME_CTX_MODE, mode.String()) + ctx = context.WithValue(ctx, runtime.RUNTIME_CTX_QUERY_PARAMS, queryParams) + + if userID != "" { + ctx = context.WithValue(ctx, runtime.RUNTIME_CTX_USER_ID, userID) + ctx = context.WithValue(ctx, runtime.RUNTIME_CTX_USERNAME, username) + ctx = context.WithValue(ctx, runtime.RUNTIME_CTX_USER_SESSION_EXP, sessionExpiry) + if sessionID != "" { + ctx = context.WithValue(ctx, runtime.RUNTIME_CTX_SESSION_ID, sessionID) + } + } + + if clientIP != "" { + ctx = context.WithValue(ctx, runtime.RUNTIME_CTX_CLIENT_IP, clientIP) + } + if clientPort != "" { + ctx = context.WithValue(ctx, runtime.RUNTIME_CTX_CLIENT_PORT, clientPort) + } + + return ctx +} diff --git a/server/runtime_go_match_core.go b/server/runtime_go_match_core.go new file mode 100644 index 0000000000000000000000000000000000000000..ea7e1281190320b03646e4ab27c68c72a33843f6 --- /dev/null +++ b/server/runtime_go_match_core.go @@ -0,0 +1,264 @@ +// Copyright 2018 The Nakama Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import ( + "context" + "database/sql" + "errors" + "fmt" + "github.com/gofrs/uuid" + "github.com/heroiclabs/nakama/rtapi" + "github.com/heroiclabs/nakama/runtime" + "go.uber.org/zap" + "log" +) + +type RuntimeGoMatchCore struct { + logger *zap.Logger + matchRegistry MatchRegistry + tracker Tracker + router MessageRouter + + labelUpdateFn func(string) + + match runtime.Match + + id uuid.UUID + node string + idStr string + stream PresenceStream + + stdLogger *log.Logger + db *sql.DB + nk runtime.NakamaModule + ctx context.Context +} + +func NewRuntimeGoMatchCore(logger *zap.Logger, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter, id uuid.UUID, node string, labelUpdateFn func(string), stdLogger *log.Logger, db *sql.DB, env map[string]string, nk runtime.NakamaModule, match runtime.Match) (RuntimeMatchCore, error) { + ctx := NewRuntimeGoContext(env, RuntimeExecutionModeMatch, nil, 0, "", "", "", "", "") + ctx = context.WithValue(ctx, runtime.RUNTIME_CTX_MATCH_ID, fmt.Sprintf("%v.%v", id.String(), node)) + ctx = context.WithValue(ctx, runtime.RUNTIME_CTX_MATCH_NODE, node) + + return &RuntimeGoMatchCore{ + logger: logger, + matchRegistry: matchRegistry, + tracker: tracker, + router: router, + + labelUpdateFn: labelUpdateFn, + + match: match, + + id: id, + node: node, + idStr: fmt.Sprintf("%v.%v", id.String(), node), + stream: PresenceStream{ + Mode: StreamModeMatchAuthoritative, + Subject: id, + Label: node, + }, + + stdLogger: stdLogger, + db: db, + nk: nk, + ctx: ctx, + }, nil +} + +func (r *RuntimeGoMatchCore) MatchInit(params map[string]interface{}) (interface{}, int, string, error) { + state, tickRate, label := r.match.MatchInit(r.ctx, r.stdLogger, r.db, r.nk, params) + + if len(label) > 256 { + return nil, 0, "", errors.New("MatchInit returned invalid label, must be 256 bytes or less") + } + if tickRate > 30 || tickRate < 1 { + return nil, 0, "", errors.New("MatchInit returned invalid tick rate, must be between 1 and 30") + } + + r.ctx = context.WithValue(r.ctx, runtime.RUNTIME_CTX_MATCH_TICK_RATE, tickRate) + r.ctx = context.WithValue(r.ctx, runtime.RUNTIME_CTX_MATCH_LABEL, label) + + return state, tickRate, label, nil +} + +func (r *RuntimeGoMatchCore) MatchJoinAttempt(tick int64, state interface{}, userID, sessionID uuid.UUID, username, node string) (interface{}, bool, string, error) { + presence := &MatchPresence{ + Node: node, + UserID: userID, + SessionID: sessionID, + Username: username, + } + + newState, allow, reason := r.match.MatchJoinAttempt(r.ctx, r.stdLogger, r.db, r.nk, r, tick, state, presence) + return newState, allow, reason, nil +} + +func (r *RuntimeGoMatchCore) MatchJoin(tick int64, state interface{}, joins []*MatchPresence) (interface{}, error) { + presences := make([]runtime.Presence, len(joins)) + for i, join := range joins { + presences[i] = runtime.Presence(join) + } + + newState := r.match.MatchJoin(r.ctx, r.stdLogger, r.db, r.nk, r, tick, state, presences) + return newState, nil +} + +func (r *RuntimeGoMatchCore) MatchLeave(tick int64, state interface{}, leaves []*MatchPresence) (interface{}, error) { + presences := make([]runtime.Presence, len(leaves)) + for i, leave := range leaves { + presences[i] = runtime.Presence(leave) + } + + newState := r.match.MatchLeave(r.ctx, r.stdLogger, r.db, r.nk, r, tick, state, presences) + return newState, nil +} + +func (r *RuntimeGoMatchCore) MatchLoop(tick int64, state interface{}, inputCh chan *MatchDataMessage) (interface{}, error) { + // Drain the input queue into a slice. + size := len(inputCh) + messages := make([]runtime.MatchData, size) + for i := 0; i < size; i++ { + msg := <-inputCh + messages[i] = runtime.MatchData(msg) + } + + newState := r.match.MatchLoop(r.ctx, r.stdLogger, r.db, r.nk, r, tick, state, messages) + return newState, nil +} + +func (r *RuntimeGoMatchCore) BroadcastMessage(opCode int64, data []byte, presences []runtime.Presence, sender runtime.Presence) error { + var presenceIDs []*PresenceID + if presences != nil { + size := len(presences) + if size == 0 { + return nil + } + + presenceIDs = make([]*PresenceID, size) + for i, presence := range presences { + sessionID, err := uuid.FromString(presence.GetSessionId()) + if err != nil { + return errors.New("Presence contains an invalid Session ID") + } + + presenceIDs[i] = &PresenceID{ + Node: presence.GetNodeId(), + SessionID: sessionID, + } + } + } + + var presence *rtapi.UserPresence + if sender != nil { + uid := sender.GetUserId() + _, err := uuid.FromString(uid) + if err != nil { + return errors.New("Sender contains an invalid User ID") + } + + sid := sender.GetSessionId() + _, err = uuid.FromString(sid) + if err != nil { + return errors.New("Sender contains an invalid Session ID") + } + + presence = &rtapi.UserPresence{ + UserId: uid, + SessionId: sid, + Username: sender.GetUsername(), + } + } + + if presenceIDs != nil { + // Ensure specific presences actually exist to prevent sending bogus messages to arbitrary users. + actualPresenceIDs := r.tracker.ListPresenceIDByStream(r.stream) + for i := 0; i < len(presenceIDs); i++ { + found := false + presenceID := presenceIDs[i] + for j := 0; j < len(actualPresenceIDs); j++ { + if actual := actualPresenceIDs[j]; presenceID.SessionID == actual.SessionID && presenceID.Node == actual.Node { + // If it matches, drop it. + actualPresenceIDs[j] = actualPresenceIDs[len(actualPresenceIDs)-1] + actualPresenceIDs = actualPresenceIDs[:len(actualPresenceIDs)-1] + found = true + break + } + } + if !found { + // If this presence wasn't in the filters, it's not needed. + presenceIDs[i] = presenceIDs[len(presenceIDs)-1] + presenceIDs = presenceIDs[:len(presenceIDs)-1] + i-- + } + } + if len(presenceIDs) == 0 { + // None of the target presenceIDs existed in the list of match members. + return nil + } + } + + msg := &rtapi.Envelope{Message: &rtapi.Envelope_MatchData{MatchData: &rtapi.MatchData{ + MatchId: r.idStr, + Presence: presence, + OpCode: opCode, + Data: data, + }}} + + if presenceIDs == nil { + r.router.SendToStream(r.logger, r.stream, msg) + } else { + r.router.SendToPresenceIDs(r.logger, presenceIDs, true, StreamModeMatchAuthoritative, msg) + } + + return nil +} + +func (r *RuntimeGoMatchCore) MatchKick(presences []runtime.Presence) error { + size := len(presences) + if size == 0 { + return nil + } + + matchPresences := make([]*MatchPresence, size) + for i, presence := range presences { + userID, err := uuid.FromString(presence.GetUserId()) + if err != nil { + return errors.New("Presence contains an invalid User ID") + } + + sessionID, err := uuid.FromString(presence.GetSessionId()) + if err != nil { + return errors.New("Presence contains an invalid Session ID") + } + + matchPresences[i] = &MatchPresence{ + Node: presence.GetNodeId(), + UserID: userID, + SessionID: sessionID, + Username: presence.GetUsername(), + } + } + + r.matchRegistry.Kick(r.stream, matchPresences) + return nil +} + +func (r *RuntimeGoMatchCore) MatchLabelUpdate(label string) error { + r.labelUpdateFn(label) + // This must be executed from inside a match call so safe to update here. + r.ctx = context.WithValue(r.ctx, runtime.RUNTIME_CTX_MATCH_LABEL, label) + return nil +} diff --git a/server/runtime_go_nakama.go b/server/runtime_go_nakama.go new file mode 100644 index 0000000000000000000000000000000000000000..3289236e5bda203d785cc2bb84e7ace9a15817da --- /dev/null +++ b/server/runtime_go_nakama.go @@ -0,0 +1,1235 @@ +// Copyright 2018 The Nakama Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import ( + "database/sql" + "encoding/json" + "github.com/gofrs/uuid" + "github.com/golang/protobuf/ptypes/timestamp" + "github.com/golang/protobuf/ptypes/wrappers" + "github.com/gorhill/cronexpr" + "github.com/heroiclabs/nakama/api" + "github.com/heroiclabs/nakama/rtapi" + "github.com/heroiclabs/nakama/runtime" + "github.com/heroiclabs/nakama/social" + "github.com/pkg/errors" + "go.uber.org/atomic" + "go.uber.org/zap" + "strings" + "sync" + "time" +) + +type RuntimeGoNakamaModule struct { + sync.RWMutex + logger *zap.Logger + db *sql.DB + config Config + socialClient *social.Client + leaderboardCache LeaderboardCache + sessionRegistry *SessionRegistry + matchRegistry MatchRegistry + tracker Tracker + router MessageRouter + + node string + + matchCreateFn RuntimeMatchCreateFunction +} + +func NewRuntimeGoNakamaModule(logger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, leaderboardCache LeaderboardCache, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter) *RuntimeGoNakamaModule { + return &RuntimeGoNakamaModule{ + logger: logger, + db: db, + config: config, + socialClient: socialClient, + leaderboardCache: leaderboardCache, + sessionRegistry: sessionRegistry, + matchRegistry: matchRegistry, + tracker: tracker, + router: router, + + node: config.GetName(), + } +} + +func (n *RuntimeGoNakamaModule) AuthenticateCustom(id, username string, create bool) (string, string, bool, error) { + if id == "" { + return "", "", false, errors.New("expects id string") + } else if invalidCharsRegex.MatchString(id) { + return "", "", false, errors.New("expects id to be valid, no spaces or control characters allowed") + } else if len(id) < 6 || len(id) > 128 { + return "", "", false, errors.New("expects id to be valid, must be 6-128 bytes") + } + + if username == "" { + username = generateUsername() + } else if invalidCharsRegex.MatchString(username) { + return "", "", false, errors.New("expects username to be valid, no spaces or control characters allowed") + } else if len(username) > 128 { + return "", "", false, errors.New("expects id to be valid, must be 1-128 bytes") + } + + return AuthenticateCustom(n.logger, n.db, id, username, create) +} + +func (n *RuntimeGoNakamaModule) AuthenticateDevice(id, username string, create bool) (string, string, bool, error) { + if id == "" { + return "", "", false, errors.New("expects id string") + } else if invalidCharsRegex.MatchString(id) { + return "", "", false, errors.New("expects id to be valid, no spaces or control characters allowed") + } else if len(id) < 10 || len(id) > 128 { + return "", "", false, errors.New("expects id to be valid, must be 10-128 bytes") + } + + if username == "" { + username = generateUsername() + } else if invalidCharsRegex.MatchString(username) { + return "", "", false, errors.New("expects username to be valid, no spaces or control characters allowed") + } else if len(username) > 128 { + return "", "", false, errors.New("expects id to be valid, must be 1-128 bytes") + } + + return AuthenticateDevice(n.logger, n.db, id, username, create) +} + +func (n *RuntimeGoNakamaModule) AuthenticateEmail(email, password, username string, create bool) (string, string, bool, error) { + if email == "" { + return "", "", false, errors.New("expects email string") + } else if invalidCharsRegex.MatchString(email) { + return "", "", false, errors.New("expects email to be valid, no spaces or control characters allowed") + } else if !emailRegex.MatchString(email) { + return "", "", false, errors.New("expects email to be valid, invalid email address format") + } else if len(email) < 10 || len(email) > 255 { + return "", "", false, errors.New("expects email to be valid, must be 10-255 bytes") + } + + if password == "" { + return "", "", false, errors.New("expects password string") + } else if len(password) < 8 { + return "", "", false, errors.New("expects password to be valid, must be longer than 8 characters") + } + + if username == "" { + username = generateUsername() + } else if invalidCharsRegex.MatchString(username) { + return "", "", false, errors.New("expects username to be valid, no spaces or control characters allowed") + } else if len(username) > 128 { + return "", "", false, errors.New("expects id to be valid, must be 1-128 bytes") + } + + cleanEmail := strings.ToLower(email) + + return AuthenticateEmail(n.logger, n.db, cleanEmail, password, username, create) +} + +func (n *RuntimeGoNakamaModule) AuthenticateFacebook(token string, importFriends bool, username string, create bool) (string, string, bool, error) { + if token == "" { + return "", "", false, errors.New("expects access token string") + } + + if username == "" { + username = generateUsername() + } else if invalidCharsRegex.MatchString(username) { + return "", "", false, errors.New("expects username to be valid, no spaces or control characters allowed") + } else if len(username) > 128 { + return "", "", false, errors.New("expects id to be valid, must be 1-128 bytes") + } + + dbUserID, dbUsername, created, err := AuthenticateFacebook(n.logger, n.db, n.socialClient, token, username, create) + if err == nil && importFriends { + importFacebookFriends(n.logger, n.db, n.router, n.socialClient, uuid.FromStringOrNil(dbUserID), dbUsername, token, false) + } + + return dbUserID, dbUsername, created, err +} + +func (n *RuntimeGoNakamaModule) AuthenticateGameCenter(playerID, bundleID string, timestamp int64, salt, signature, publicKeyUrl, username string, create bool) (string, string, bool, error) { + if playerID == "" { + return "", "", false, errors.New("expects player ID string") + } + if bundleID == "" { + return "", "", false, errors.New("expects bundle ID string") + } + if timestamp == 0 { + return "", "", false, errors.New("expects timestamp value") + } + if salt == "" { + return "", "", false, errors.New("expects salt string") + } + if signature == "" { + return "", "", false, errors.New("expects signature string") + } + if publicKeyUrl == "" { + return "", "", false, errors.New("expects public key URL string") + } + + if username == "" { + username = generateUsername() + } else if invalidCharsRegex.MatchString(username) { + return "", "", false, errors.New("expects username to be valid, no spaces or control characters allowed") + } else if len(username) > 128 { + return "", "", false, errors.New("expects id to be valid, must be 1-128 bytes") + } + + return AuthenticateGameCenter(n.logger, n.db, n.socialClient, playerID, bundleID, timestamp, salt, signature, publicKeyUrl, username, create) +} + +func (n *RuntimeGoNakamaModule) AuthenticateGoogle(token, username string, create bool) (string, string, bool, error) { + if token == "" { + return "", "", false, errors.New("expects ID token string") + } + + if username == "" { + username = generateUsername() + } else if invalidCharsRegex.MatchString(username) { + return "", "", false, errors.New("expects username to be valid, no spaces or control characters allowed") + } else if len(username) > 128 { + return "", "", false, errors.New("expects id to be valid, must be 1-128 bytes") + } + + return AuthenticateGoogle(n.logger, n.db, n.socialClient, token, username, create) +} + +func (n *RuntimeGoNakamaModule) AuthenticateSteam(token, username string, create bool) (string, string, bool, error) { + if n.config.GetSocial().Steam.PublisherKey == "" || n.config.GetSocial().Steam.AppID == 0 { + return "", "", false, errors.New("Steam authentication is not configured") + } + + if token == "" { + return "", "", false, errors.New("expects token string") + } + + if username == "" { + username = generateUsername() + } else if invalidCharsRegex.MatchString(username) { + return "", "", false, errors.New("expects username to be valid, no spaces or control characters allowed") + } else if len(username) > 128 { + return "", "", false, errors.New("expects id to be valid, must be 1-128 bytes") + } + + return AuthenticateSteam(n.logger, n.db, n.socialClient, n.config.GetSocial().Steam.AppID, n.config.GetSocial().Steam.PublisherKey, token, username, create) +} + +func (n *RuntimeGoNakamaModule) AuthenticateTokenGenerate(userID, username string, exp int64) (string, int64, error) { + if userID == "" { + return "", 0, errors.New("expects user id") + } + _, err := uuid.FromString(userID) + if err != nil { + return "", 0, errors.New("expects valid user id") + } + + if username == "" { + return "", 0, errors.New("expects username") + } + + if exp == 0 { + // If expiry is 0 or not set, use standard configured expiry. + exp = time.Now().UTC().Add(time.Duration(n.config.GetSession().TokenExpirySec) * time.Second).Unix() + } + + token, exp := generateTokenWithExpiry(n.config, userID, username, exp) + return token, exp, nil +} + +func (n *RuntimeGoNakamaModule) AccountGetId(userID string) (*api.Account, error) { + u, err := uuid.FromString(userID) + if err != nil { + return nil, errors.New("invalid user id") + } + + return GetAccount(n.logger, n.db, n.tracker, u) +} + +func (n *RuntimeGoNakamaModule) AccountUpdateId(userID, username string, metadata map[string]interface{}, displayName, timezone, location, langTag, avatarUrl string) error { + u, err := uuid.FromString(userID) + if err != nil { + return errors.New("expects user ID to be a valid identifier") + } + + var metadataWrapper *wrappers.StringValue + if metadata != nil { + metadataBytes, err := json.Marshal(metadata) + if err != nil { + return errors.Errorf("error encoding metadata: %v", err.Error()) + } + metadataWrapper = &wrappers.StringValue{Value: string(metadataBytes)} + } + + displayNameWrapper := &wrappers.StringValue{Value: displayName} + timezoneWrapper := &wrappers.StringValue{Value: timezone} + locationWrapper := &wrappers.StringValue{Value: location} + langWrapper := &wrappers.StringValue{Value: langTag} + avatarWrapper := &wrappers.StringValue{Value: avatarUrl} + + return UpdateAccount(n.db, n.logger, u, username, displayNameWrapper, timezoneWrapper, locationWrapper, langWrapper, avatarWrapper, metadataWrapper) +} + +func (n *RuntimeGoNakamaModule) UsersGetId(userIDs []string) ([]*api.User, error) { + if len(userIDs) == 0 { + return make([]*api.User, 0), nil + } + + for _, id := range userIDs { + if _, err := uuid.FromString(id); err != nil { + return nil, errors.New("each user id must be a valid id string") + } + } + + users, err := GetUsers(n.logger, n.db, n.tracker, userIDs, nil, nil) + if err != nil { + return nil, err + } + + return users.Users, nil +} + +func (n *RuntimeGoNakamaModule) UsersGetUsername(usernames []string) ([]*api.User, error) { + if len(usernames) == 0 { + return make([]*api.User, 0), nil + } + + for _, username := range usernames { + if username == "" { + return nil, errors.New("each username must be a string") + } + } + + users, err := GetUsers(n.logger, n.db, n.tracker, nil, usernames, nil) + if err != nil { + return nil, err + } + + return users.Users, nil +} + +func (n *RuntimeGoNakamaModule) UsersBanId(userIDs []string) error { + if len(userIDs) == 0 { + return nil + } + + for _, id := range userIDs { + if _, err := uuid.FromString(id); err != nil { + return errors.New("each user id must be a valid id string") + } + } + + return BanUsers(n.logger, n.db, userIDs) +} + +func (n *RuntimeGoNakamaModule) UsersUnbanId(userIDs []string) error { + if len(userIDs) == 0 { + return nil + } + + for _, id := range userIDs { + if _, err := uuid.FromString(id); err != nil { + return errors.New("each user id must be a valid id string") + } + } + + return UnbanUsers(n.logger, n.db, userIDs) +} + +func (n *RuntimeGoNakamaModule) StreamUserList(mode uint8, subject, descriptor, label string, includeHidden, includeNotHidden bool) ([]runtime.Presence, error) { + stream := PresenceStream{ + Mode: mode, + Label: label, + } + var err error + if subject != "" { + stream.Subject, err = uuid.FromString(subject) + if err != nil { + return nil, errors.New("stream subject must be a valid identifier") + } + } + if descriptor != "" { + stream.Descriptor, err = uuid.FromString(descriptor) + if err != nil { + return nil, errors.New("stream descriptor must be a valid identifier") + } + } + + presences := n.tracker.ListByStream(stream, includeHidden, includeNotHidden) + runtimePresences := make([]runtime.Presence, len(presences)) + for i, p := range presences { + runtimePresences[i] = runtime.Presence(p) + } + return runtimePresences, nil +} + +func (n *RuntimeGoNakamaModule) StreamUserGet(mode uint8, subject, descriptor, label, userID, sessionID string) (runtime.PresenceMeta, error) { + uid, err := uuid.FromString(userID) + if err != nil { + return nil, errors.New("expects valid user id") + } + + sid, err := uuid.FromString(sessionID) + if err != nil { + return nil, errors.New("expects valid session id") + } + + stream := PresenceStream{ + Mode: mode, + Label: label, + } + if subject != "" { + stream.Subject, err = uuid.FromString(subject) + if err != nil { + return nil, errors.New("stream subject must be a valid identifier") + } + } + if descriptor != "" { + stream.Descriptor, err = uuid.FromString(descriptor) + if err != nil { + return nil, errors.New("stream descriptor must be a valid identifier") + } + } + + if meta := n.tracker.GetLocalBySessionIDStreamUserID(sid, stream, uid); meta != nil { + return meta, nil + } + return nil, nil +} + +func (n *RuntimeGoNakamaModule) StreamUserJoin(mode uint8, subject, descriptor, label, userID, sessionID string, hidden, persistence bool, status string) (bool, error) { + uid, err := uuid.FromString(userID) + if err != nil { + return false, errors.New("expects valid user id") + } + + sid, err := uuid.FromString(sessionID) + if err != nil { + return false, errors.New("expects valid session id") + } + + stream := PresenceStream{ + Mode: mode, + Label: label, + } + if subject != "" { + stream.Subject, err = uuid.FromString(subject) + if err != nil { + return false, errors.New("stream subject must be a valid identifier") + } + } + if descriptor != "" { + stream.Descriptor, err = uuid.FromString(descriptor) + if err != nil { + return false, errors.New("stream descriptor must be a valid identifier") + } + } + + // Look up the session. + session := n.sessionRegistry.Get(sid) + if session == nil { + return false, errors.New("session id does not exist") + } + + success, newlyTracked := n.tracker.Track(sid, stream, uid, PresenceMeta{ + Format: session.Format(), + Hidden: hidden, + Persistence: persistence, + Username: session.Username(), + Status: status, + }, false) + if !success { + return false, errors.New("tracker rejected new presence, session is closing") + } + + return newlyTracked, nil +} + +func (n *RuntimeGoNakamaModule) StreamUserUpdate(mode uint8, subject, descriptor, label, userID, sessionID string, hidden, persistence bool, status string) error { + uid, err := uuid.FromString(userID) + if err != nil { + return errors.New("expects valid user id") + } + + sid, err := uuid.FromString(sessionID) + if err != nil { + return errors.New("expects valid session id") + } + + stream := PresenceStream{ + Mode: mode, + Label: label, + } + if subject != "" { + stream.Subject, err = uuid.FromString(subject) + if err != nil { + return errors.New("stream subject must be a valid identifier") + } + } + if descriptor != "" { + stream.Descriptor, err = uuid.FromString(descriptor) + if err != nil { + return errors.New("stream descriptor must be a valid identifier") + } + } + + // Look up the session. + session := n.sessionRegistry.Get(sid) + if session == nil { + return errors.New("session id does not exist") + } + + if !n.tracker.Update(sid, stream, uid, PresenceMeta{ + Format: session.Format(), + Hidden: hidden, + Persistence: persistence, + Username: session.Username(), + Status: status, + }, false) { + return errors.New("tracker rejected updated presence, session is closing") + } + + return nil +} + +func (n *RuntimeGoNakamaModule) StreamUserLeave(mode uint8, subject, descriptor, label, userID, sessionID string) error { + uid, err := uuid.FromString(userID) + if err != nil { + return errors.New("expects valid user id") + } + + sid, err := uuid.FromString(sessionID) + if err != nil { + return errors.New("expects valid session id") + } + + stream := PresenceStream{ + Mode: mode, + Label: label, + } + if subject != "" { + stream.Subject, err = uuid.FromString(subject) + if err != nil { + return errors.New("stream subject must be a valid identifier") + } + } + if descriptor != "" { + stream.Descriptor, err = uuid.FromString(descriptor) + if err != nil { + return errors.New("stream descriptor must be a valid identifier") + } + } + + n.tracker.Untrack(sid, stream, uid) + + return nil +} + +func (n *RuntimeGoNakamaModule) StreamCount(mode uint8, subject, descriptor, label string) (int, error) { + stream := PresenceStream{ + Mode: mode, + Label: label, + } + var err error + if subject != "" { + stream.Subject, err = uuid.FromString(subject) + if err != nil { + return 0, errors.New("stream subject must be a valid identifier") + } + } + if descriptor != "" { + stream.Descriptor, err = uuid.FromString(descriptor) + if err != nil { + return 0, errors.New("stream descriptor must be a valid identifier") + } + } + + return n.tracker.CountByStream(stream), nil +} + +func (n *RuntimeGoNakamaModule) StreamClose(mode uint8, subject, descriptor, label string) error { + stream := PresenceStream{ + Mode: mode, + Label: label, + } + var err error + if subject != "" { + stream.Subject, err = uuid.FromString(subject) + if err != nil { + return errors.New("stream subject must be a valid identifier") + } + } + if descriptor != "" { + stream.Descriptor, err = uuid.FromString(descriptor) + if err != nil { + return errors.New("stream descriptor must be a valid identifier") + } + } + + n.tracker.UntrackByStream(stream) + + return nil +} + +func (n *RuntimeGoNakamaModule) StreamSend(mode uint8, subject, descriptor, label, data string) error { + stream := PresenceStream{ + Mode: mode, + Label: label, + } + var err error + if subject != "" { + stream.Subject, err = uuid.FromString(subject) + if err != nil { + return errors.New("stream subject must be a valid identifier") + } + } + if descriptor != "" { + stream.Descriptor, err = uuid.FromString(descriptor) + if err != nil { + return errors.New("stream descriptor must be a valid identifier") + } + } + + streamWire := &rtapi.Stream{ + Mode: int32(stream.Mode), + Label: stream.Label, + } + if stream.Subject != uuid.Nil { + streamWire.Subject = stream.Subject.String() + } + if stream.Descriptor != uuid.Nil { + streamWire.Descriptor_ = stream.Descriptor.String() + } + msg := &rtapi.Envelope{Message: &rtapi.Envelope_StreamData{StreamData: &rtapi.StreamData{ + Stream: streamWire, + // No sender. + Data: data, + }}} + n.router.SendToStream(n.logger, stream, msg) + + return nil +} + +func (n *RuntimeGoNakamaModule) MatchCreate(module string, params map[string]interface{}) (string, error) { + if module == "" { + return "", errors.New("expects module name") + } + + id := uuid.Must(uuid.NewV4()) + matchLogger := n.logger.With(zap.String("mid", id.String())) + label := atomic.NewString("") + labelUpdateFn := func(input string) { + label.Store(input) + } + n.RLock() + fn := n.matchCreateFn + n.RUnlock() + core, err := fn(matchLogger, id, n.node, module, labelUpdateFn) + if err != nil { + return "", err + } + if core == nil { + return "", errors.New("error creating match: not found") + } + + // Start the match. + mh, err := n.matchRegistry.NewMatch(matchLogger, id, label, core, params) + if err != nil { + return "", errors.Errorf("error creating match: %v", err.Error()) + } + + return mh.IDStr, nil +} + +func (n *RuntimeGoNakamaModule) MatchList(limit int, authoritative bool, label string, minSize, maxSize int) []*api.Match { + authoritativeWrapper := &wrappers.BoolValue{Value: authoritative} + labelWrapper := &wrappers.StringValue{Value: label} + minSizeWrapper := &wrappers.Int32Value{Value: int32(minSize)} + maxSizeWrapper := &wrappers.Int32Value{Value: int32(maxSize)} + + return n.matchRegistry.ListMatches(limit, authoritativeWrapper, labelWrapper, minSizeWrapper, maxSizeWrapper) +} + +func (n *RuntimeGoNakamaModule) NotificationSend(userID, subject string, content map[string]interface{}, code int, sender string, persistent bool) error { + uid, err := uuid.FromString(userID) + if err != nil { + return errors.New("expects userID to be a valid UUID") + } + + if subject == "" { + return errors.New("expects subject to be a non-empty string") + } + + contentBytes, err := json.Marshal(content) + if err != nil { + return errors.Errorf("failed to convert content: %s", err.Error()) + } + contentString := string(contentBytes) + + if code <= 0 { + return errors.New("expects code to number above 0") + } + + senderID := uuid.Nil.String() + if sender != "" { + suid, err := uuid.FromString(sender) + if err != nil { + return errors.New("expects sender to either be an empty string or a valid UUID") + } + senderID = suid.String() + } + + nots := []*api.Notification{{ + Id: uuid.Must(uuid.NewV4()).String(), + Subject: subject, + Content: contentString, + Code: int32(code), + SenderId: senderID, + Persistent: persistent, + CreateTime: ×tamp.Timestamp{Seconds: time.Now().UTC().Unix()}, + }} + notifications := map[uuid.UUID][]*api.Notification{ + uid: nots, + } + + return NotificationSend(n.logger, n.db, n.router, notifications) +} + +func (n *RuntimeGoNakamaModule) NotificationsSend(notifications []*runtime.NotificationSend) error { + ns := make(map[uuid.UUID][]*api.Notification) + + for _, notification := range notifications { + uid, err := uuid.FromString(notification.UserID) + if err != nil { + return errors.New("expects userID to be a valid UUID") + } + + if notification.Subject == "" { + return errors.New("expects subject to be a non-empty string") + } + + contentBytes, err := json.Marshal(notification.Content) + if err != nil { + return errors.Errorf("failed to convert content: %s", err.Error()) + } + contentString := string(contentBytes) + + if notification.Code <= 0 { + return errors.New("expects code to number above 0") + } + + senderID := uuid.Nil.String() + if notification.Sender != "" { + suid, err := uuid.FromString(notification.Sender) + if err != nil { + return errors.New("expects sender to either be an empty string or a valid UUID") + } + senderID = suid.String() + } + + no := ns[uid] + if no == nil { + no = make([]*api.Notification, 0) + } + no = append(no, &api.Notification{ + Id: uuid.Must(uuid.NewV4()).String(), + Subject: notification.Subject, + Content: contentString, + Code: int32(notification.Code), + SenderId: senderID, + Persistent: notification.Persistent, + CreateTime: ×tamp.Timestamp{Seconds: time.Now().UTC().Unix()}, + }) + ns[uid] = no + } + + return NotificationSend(n.logger, n.db, n.router, ns) +} + +func (n *RuntimeGoNakamaModule) WalletUpdate(userID string, changeset, metadata map[string]interface{}) error { + uid, err := uuid.FromString(userID) + if err != nil { + return errors.New("expects a valid user id") + } + + metadataBytes := []byte("{}") + if metadata != nil { + metadataBytes, err = json.Marshal(metadata) + if err != nil { + return errors.Errorf("failed to convert metadata: %s", err.Error()) + } + } + + return UpdateWallets(n.logger, n.db, []*walletUpdate{&walletUpdate{ + UserID: uid, + Changeset: changeset, + Metadata: string(metadataBytes), + }}) +} + +func (n *RuntimeGoNakamaModule) WalletsUpdate(updates []*runtime.WalletUpdate) error { + size := len(updates) + if size == 0 { + return nil + } + + walletUpdates := make([]*walletUpdate, size) + + for i, update := range updates { + uid, err := uuid.FromString(update.UserID) + if err != nil { + return errors.New("expects a valid user id") + } + + metadataBytes := []byte("{}") + if update.Metadata != nil { + metadataBytes, err = json.Marshal(update.Metadata) + if err != nil { + return errors.Errorf("failed to convert metadata: %s", err.Error()) + } + } + + walletUpdates[i] = &walletUpdate{ + UserID: uid, + Changeset: update.Changeset, + Metadata: string(metadataBytes), + } + } + + return UpdateWallets(n.logger, n.db, walletUpdates) +} + +func (n *RuntimeGoNakamaModule) WalletLedgerUpdate(itemID string, metadata map[string]interface{}) (runtime.WalletLedgerItem, error) { + id, err := uuid.FromString(itemID) + if err != nil { + return nil, errors.New("expects a valid item id") + } + + metadataBytes, err := json.Marshal(metadata) + if err != nil { + return nil, errors.Errorf("failed to convert metadata: %s", err.Error()) + } + + return UpdateWalletLedger(n.logger, n.db, id, string(metadataBytes)) +} + +func (n *RuntimeGoNakamaModule) WalletLedgerList(userID string) ([]runtime.WalletLedgerItem, error) { + uid, err := uuid.FromString(userID) + if err != nil { + return nil, errors.New("expects a valid user id") + } + + items, err := ListWalletLedger(n.logger, n.db, uid) + if err != nil { + return nil, err + } + + runtimeItems := make([]runtime.WalletLedgerItem, len(items)) + for i, item := range items { + runtimeItems[i] = runtime.WalletLedgerItem(item) + } + return runtimeItems, nil +} + +func (n *RuntimeGoNakamaModule) StorageList(userID, collection string, limit int, cursor string) ([]*api.StorageObject, string, error) { + uid := uuid.Nil + var err error + if userID != "" { + uid, err = uuid.FromString(userID) + if err != nil { + return nil, "", errors.New("expects an empty or valid user id") + } + } + + objectList, _, err := StorageListObjects(n.logger, n.db, uuid.Nil, uid, collection, limit, cursor) + if err != nil { + return nil, "", err + } + + return objectList.Objects, objectList.Cursor, nil +} + +func (n *RuntimeGoNakamaModule) StorageRead(reads []*runtime.StorageRead) ([]*api.StorageObject, error) { + size := len(reads) + if size == 0 { + return make([]*api.StorageObject, 0), nil + } + objectIDs := make([]*api.ReadStorageObjectId, size) + + for i, read := range reads { + if read.Collection == "" { + return nil, errors.New("expects collection to be a non-empty string") + } + if read.Key == "" { + return nil, errors.New("expects key to be a non-empty string") + } + uid := uuid.Nil + var err error + if read.UserID != "" { + uid, err = uuid.FromString(read.UserID) + if err != nil { + return nil, errors.New("expects an empty or valid user id") + } + } + + objectIDs[i] = &api.ReadStorageObjectId{ + Collection: read.Collection, + Key: read.Key, + UserId: uid.String(), + } + } + + objects, err := StorageReadObjects(n.logger, n.db, uuid.Nil, objectIDs) + if err != nil { + return nil, err + } + + return objects.Objects, nil +} + +func (n *RuntimeGoNakamaModule) StorageWrite(writes []*runtime.StorageWrite) ([]*api.StorageObjectAck, error) { + size := len(writes) + if size == 0 { + return make([]*api.StorageObjectAck, 0), nil + } + + data := make(map[uuid.UUID][]*api.WriteStorageObject) + + for _, write := range writes { + if write.Collection == "" { + return nil, errors.New("expects collection to be a non-empty string") + } + if write.Key == "" { + return nil, errors.New("expects key to be a non-empty string") + } + uid := uuid.Nil + var err error + if write.UserID != "" { + uid, err = uuid.FromString(write.UserID) + if err != nil { + return nil, errors.New("expects an empty or valid user id") + } + } + var valueMap map[string]interface{} + err = json.Unmarshal([]byte(write.Value), valueMap) + if err != nil { + return nil, errors.New("value must be a JSON-encoded object") + } + + d := &api.WriteStorageObject{ + Collection: write.Collection, + Key: write.Key, + Value: write.Value, + Version: write.Version, + PermissionRead: &wrappers.Int32Value{Value: int32(write.PermissionRead)}, + PermissionWrite: &wrappers.Int32Value{Value: int32(write.PermissionWrite)}, + } + + if objects, ok := data[uid]; !ok { + data[uid] = []*api.WriteStorageObject{d} + } else { + data[uid] = append(objects, d) + } + } + + acks, _, err := StorageWriteObjects(n.logger, n.db, true, data) + if err != nil { + return nil, err + } + + return acks.Acks, nil +} + +func (n *RuntimeGoNakamaModule) StorageDelete(deletes []*runtime.StorageDelete) error { + size := len(deletes) + if size == 0 { + return nil + } + + objectIDs := make(map[uuid.UUID][]*api.DeleteStorageObjectId) + + for _, del := range deletes { + if del.Collection == "" { + return errors.New("expects collection to be a non-empty string") + } + if del.Key == "" { + return errors.New("expects key to be a non-empty string") + } + uid := uuid.Nil + var err error + if del.UserID != "" { + uid, err = uuid.FromString(del.UserID) + if err != nil { + return errors.New("expects an empty or valid user id") + } + } + + objectID := &api.DeleteStorageObjectId{ + Collection: del.Collection, + Key: del.Key, + Version: del.Version, + } + + if objects, ok := objectIDs[uid]; !ok { + objectIDs[uid] = []*api.DeleteStorageObjectId{objectID} + } else { + objectIDs[uid] = append(objects, objectID) + } + } + + _, err := StorageDeleteObjects(n.logger, n.db, true, objectIDs) + + return err +} + +func (n *RuntimeGoNakamaModule) LeaderboardCreate(id string, authoritative bool, sortOrder, operator, resetSchedule string, metadata map[string]interface{}) error { + if id == "" { + return errors.New("expects a leaderboard ID string") + } + + sort := LeaderboardSortOrderDescending + switch sortOrder { + case "desc": + sort = LeaderboardSortOrderDescending + case "asc": + sort = LeaderboardSortOrderAscending + default: + return errors.New("expects sort order to be 'asc' or 'desc'") + } + + oper := LeaderboardOperatorBest + switch operator { + case "best": + oper = LeaderboardOperatorBest + case "set": + oper = LeaderboardOperatorSet + case "incr": + oper = LeaderboardOperatorIncrement + default: + return errors.New("expects sort order to be 'best', 'set', or 'incr'") + } + + if resetSchedule != "" { + if _, err := cronexpr.Parse(resetSchedule); err != nil { + return errors.New("expects reset schedule to be a valid CRON expression") + } + } + + metadataStr := "{}" + if metadata != nil { + metadataBytes, err := json.Marshal(metadata) + if err != nil { + return errors.Errorf("error encoding metadata: %v", err.Error()) + } + metadataStr = string(metadataBytes) + } + + return n.leaderboardCache.Create(id, authoritative, sort, oper, resetSchedule, metadataStr) +} + +func (n *RuntimeGoNakamaModule) LeaderboardDelete(id string) error { + if id == "" { + return errors.New("expects a leaderboard ID string") + } + + return n.leaderboardCache.Delete(id) +} + +func (n *RuntimeGoNakamaModule) LeaderboardRecordsList(id string, ownerIDs []string, limit int, cursor string) ([]*api.LeaderboardRecord, []*api.LeaderboardRecord, string, string, error) { + if id == "" { + return nil, nil, "", "", errors.New("expects a leaderboard ID string") + } + + for _, o := range ownerIDs { + if _, err := uuid.FromString(o); err != nil { + return nil, nil, "", "", errors.New("expects each owner ID to be a valid identifier") + } + } + + list, err := LeaderboardRecordsList(n.logger, n.db, n.leaderboardCache, id, &wrappers.Int32Value{Value: int32(limit)}, cursor, ownerIDs) + if err != nil { + return nil, nil, "", "", err + } + + return list.Records, list.OwnerRecords, list.NextCursor, list.PrevCursor, nil +} + +func (n *RuntimeGoNakamaModule) LeaderboardRecordWrite(id, ownerID, username string, score, subscore int64, metadata map[string]interface{}) (*api.LeaderboardRecord, error) { + if id == "" { + return nil, errors.New("expects a leaderboard ID string") + } + + if _, err := uuid.FromString(ownerID); err != nil { + return nil, errors.New("expects owner ID to be a valid identifier") + } + + if score < 0 { + return nil, errors.New("expects score to be >= 0") + } + if subscore < 0 { + return nil, errors.New("expects subscore to be >= 0") + } + + metadataStr := "" + if metadata != nil { + metadataBytes, err := json.Marshal(metadata) + if err != nil { + return nil, errors.Errorf("error encoding metadata: %v", err.Error()) + } + metadataStr = string(metadataBytes) + } + + return LeaderboardRecordWrite(n.logger, n.db, n.leaderboardCache, uuid.Nil, id, ownerID, username, score, subscore, metadataStr) +} + +func (n *RuntimeGoNakamaModule) LeaderboardRecordDelete(id, ownerID string) error { + if id == "" { + return errors.New("expects a leaderboard ID string") + } + + if _, err := uuid.FromString(ownerID); err != nil { + return errors.New("expects owner ID to be a valid identifier") + } + + return LeaderboardRecordDelete(n.logger, n.db, n.leaderboardCache, uuid.Nil, id, ownerID) +} + +func (n *RuntimeGoNakamaModule) GroupCreate(userID, name, creatorID, langTag, description, avatarUrl string, open bool, metadata map[string]interface{}, maxCount int) (*api.Group, error) { + uid, err := uuid.FromString(userID) + if err != nil { + return nil, errors.New("expects user ID to be a valid identifier") + } + + if name == "" { + return nil, errors.New("expects group name not be empty") + } + + cid := uuid.Nil + if creatorID != "" { + cid, err = uuid.FromString(creatorID) + if err != nil { + return nil, errors.New("expects creator ID to be a valid identifier") + } + } + + metadataStr := "" + if metadata != nil { + metadataBytes, err := json.Marshal(metadata) + if err != nil { + return nil, errors.Errorf("error encoding metadata: %v", err.Error()) + } + metadataStr = string(metadataBytes) + } + + if maxCount < 0 || maxCount > 100 { + return nil, errors.New("expects max_count to be > 0 and <= 100") + } + + return CreateGroup(n.logger, n.db, uid, cid, name, langTag, description, avatarUrl, metadataStr, open, maxCount) +} + +func (n *RuntimeGoNakamaModule) GroupUpdate(id, name, creatorID, langTag, description, avatarUrl string, open bool, metadata map[string]interface{}, maxCount int) error { + groupID, err := uuid.FromString(id) + if err != nil { + return errors.New("expects group ID to be a valid identifier") + } + + var nameWrapper *wrappers.StringValue + if name != "" { + nameWrapper = &wrappers.StringValue{Value: name} + } + + var creatorIDByte []byte + if creatorID != "" { + cuid, err := uuid.FromString(creatorID) + if err != nil { + return errors.New("expects creator ID to be a valid identifier") + } + creatorIDByte = cuid.Bytes() + } + + var langTagWrapper *wrappers.StringValue + if langTag != "" { + langTagWrapper = &wrappers.StringValue{Value: langTag} + } + + var descriptionWrapper *wrappers.StringValue + if description != "" { + descriptionWrapper = &wrappers.StringValue{Value: description} + } + + var avatarUrlWrapper *wrappers.StringValue + if avatarUrl != "" { + avatarUrlWrapper = &wrappers.StringValue{Value: avatarUrl} + } + + openWrapper := &wrappers.BoolValue{Value: open} + + var metadataWrapper *wrappers.StringValue + if metadata != nil { + metadataBytes, err := json.Marshal(metadata) + if err != nil { + return errors.Errorf("error encoding metadata: %v", err.Error()) + } + metadataWrapper = &wrappers.StringValue{Value: string(metadataBytes)} + } + + maxCountValue := 0 + if maxCount > 0 && maxCount <= 100 { + maxCountValue = maxCount + } + + return UpdateGroup(n.logger, n.db, groupID, uuid.Nil, creatorIDByte, nameWrapper, langTagWrapper, descriptionWrapper, avatarUrlWrapper, metadataWrapper, openWrapper, maxCountValue) +} + +func (n *RuntimeGoNakamaModule) GroupDelete(id string) error { + groupID, err := uuid.FromString(id) + if err != nil { + return errors.New("expects group ID to be a valid identifier") + } + + return DeleteGroup(n.logger, n.db, groupID, uuid.Nil) +} + +func (n *RuntimeGoNakamaModule) GroupUsersList(id string) ([]*api.GroupUserList_GroupUser, error) { + groupID, err := uuid.FromString(id) + if err != nil { + return nil, errors.New("expects group ID to be a valid identifier") + } + + users, err := ListGroupUsers(n.logger, n.db, n.tracker, groupID) + if err != nil { + return nil, err + } + + return users.GroupUsers, nil +} + +func (n *RuntimeGoNakamaModule) UserGroupsList(userID string) ([]*api.UserGroupList_UserGroup, error) { + uid, err := uuid.FromString(userID) + if err != nil { + return nil, errors.New("expects user ID to be a valid identifier") + } + + groups, err := ListUserGroups(n.logger, n.db, uid) + if err != nil { + return nil, err + } + + return groups.UserGroups, nil +} + +func (n *RuntimeGoNakamaModule) SetMatchCreateFn(fn RuntimeMatchCreateFunction) { + n.Lock() + n.matchCreateFn = fn + n.Unlock() +} diff --git a/server/runtime_hook.go b/server/runtime_hook.go deleted file mode 100644 index 811ce81d51dbd1545e49cf69fa34503b9fc05725..0000000000000000000000000000000000000000 --- a/server/runtime_hook.go +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright 2018 The Nakama Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package server - -import ( - "encoding/json" - "strings" - - "github.com/gofrs/uuid" - "github.com/golang/protobuf/jsonpb" - "github.com/golang/protobuf/proto" - "github.com/yuin/gopher-lua" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -func invokeReqBeforeHook(logger *zap.Logger, config Config, runtimePool *RuntimePool, jsonpbMarshaler *jsonpb.Marshaler, jsonpbUnmarshaler *jsonpb.Unmarshaler, sessionID string, uid uuid.UUID, username string, expiry int64, clientIP string, clientPort string, callbackID string, req interface{}) (interface{}, error) { - id := strings.ToLower(callbackID) - if !runtimePool.HasCallback(ExecutionModeBefore, id) { - return req, nil - } - - runtime := runtimePool.Get() - lf := runtime.GetCallback(ExecutionModeBefore, id) - if lf == nil { - runtimePool.Put(runtime) - logger.Error("Expected runtime Before function but didn't find it.", zap.String("id", id)) - return nil, status.Error(codes.NotFound, "Runtime Before function not found.") - } - - reqProto, ok := req.(proto.Message) - if !ok { - runtimePool.Put(runtime) - logger.Error("Could not cast request to message", zap.Any("request", req)) - return nil, status.Error(codes.Internal, "Could not run runtime Before function.") - } - reqJSON, err := jsonpbMarshaler.MarshalToString(reqProto) - if err != nil { - runtimePool.Put(runtime) - logger.Error("Could not marshall request to JSON", zap.Any("request", req), zap.Error(err)) - return nil, status.Error(codes.Internal, "Could not run runtime Before function.") - } - var reqMap map[string]interface{} - if err := json.Unmarshal([]byte(reqJSON), &reqMap); err != nil { - runtimePool.Put(runtime) - logger.Error("Could not unmarshall request to interface{}", zap.Any("request_json", reqJSON), zap.Error(err)) - return nil, status.Error(codes.Internal, "Could not run runtime Before function.") - } - - userID := "" - if uid != uuid.Nil { - userID = uid.String() - } - result, fnErr, code := runtime.InvokeFunction(ExecutionModeBefore, lf, nil, userID, username, expiry, sessionID, clientIP, clientPort, reqMap) - runtimePool.Put(runtime) - - if fnErr != nil { - logger.Error("Runtime Before function caused an error.", zap.String("id", id), zap.Error(fnErr)) - if apiErr, ok := fnErr.(*lua.ApiError); ok && !logger.Core().Enabled(zapcore.InfoLevel) { - msg := apiErr.Object.String() - if strings.HasPrefix(msg, lf.Proto.SourceName) { - msg = msg[len(lf.Proto.SourceName):] - msgParts := strings.SplitN(msg, ": ", 2) - if len(msgParts) == 2 { - msg = msgParts[1] - } else { - msg = msgParts[0] - } - } - return nil, status.Error(code, msg) - } else { - return nil, status.Error(code, fnErr.Error()) - } - } - - if result == nil { - return nil, nil - } - - resultJSON, err := json.Marshal(result) - if err != nil { - logger.Error("Could not marshall result to JSON", zap.Any("result", result), zap.Error(err)) - return nil, status.Error(codes.Internal, "Could not complete runtime Before function.") - } - - if err = jsonpbUnmarshaler.Unmarshal(strings.NewReader(string(resultJSON)), reqProto); err != nil { - logger.Error("Could not marshall result to JSON", zap.Any("result", result), zap.Error(err)) - return nil, status.Error(codes.Internal, "Could not complete runtime Before function.") - } - - return reqProto, nil -} - -func invokeReqAfterHook(logger *zap.Logger, config Config, runtimePool *RuntimePool, jsonpbMarshaler *jsonpb.Marshaler, sessionID string, uid uuid.UUID, username string, expiry int64, clientIP string, clientPort string, callbackID string, req interface{}) { - id := strings.ToLower(callbackID) - if !runtimePool.HasCallback(ExecutionModeAfter, id) { - return - } - - runtime := runtimePool.Get() - lf := runtime.GetCallback(ExecutionModeAfter, id) - if lf == nil { - runtimePool.Put(runtime) - logger.Error("Expected runtime After function but didn't find it.", zap.String("id", id)) - return - } - - reqProto, ok := req.(proto.Message) - if !ok { - runtimePool.Put(runtime) - logger.Error("Could not cast request to message", zap.Any("request", req)) - return - } - reqJSON, err := jsonpbMarshaler.MarshalToString(reqProto) - if err != nil { - runtimePool.Put(runtime) - logger.Error("Could not marshall request to JSON", zap.Any("request", req), zap.Error(err)) - return - } - - var reqMap map[string]interface{} - if err := json.Unmarshal([]byte(reqJSON), &reqMap); err != nil { - runtimePool.Put(runtime) - logger.Error("Could not unmarshall request to interface{}", zap.Any("request_json", reqJSON), zap.Error(err)) - return - } - - userID := "" - if uid != uuid.Nil { - userID = uid.String() - } - _, fnErr, _ := runtime.InvokeFunction(ExecutionModeAfter, lf, nil, userID, username, expiry, sessionID, clientIP, clientPort, reqMap) - runtimePool.Put(runtime) - - if fnErr != nil { - logger.Error("Runtime After function caused an error.", zap.String("id", id), zap.Error(fnErr)) - if apiErr, ok := fnErr.(*lua.ApiError); ok && !logger.Core().Enabled(zapcore.InfoLevel) { - msg := apiErr.Object.String() - if strings.HasPrefix(msg, lf.Proto.SourceName) { - msg = msg[len(lf.Proto.SourceName):] - msgParts := strings.SplitN(msg, ": ", 2) - if len(msgParts) == 2 { - msg = msgParts[1] - } else { - msg = msgParts[0] - } - } - } - } -} - -func invokeMatchmakerMatchedHook(logger *zap.Logger, runtimePool *RuntimePool, entries []*MatchmakerEntry) (string, bool) { - if !runtimePool.HasCallback(ExecutionModeMatchmaker, "") { - return "", false - } - - runtime := runtimePool.Get() - lf := runtime.GetCallback(ExecutionModeMatchmaker, "") - if lf == nil { - runtimePool.Put(runtime) - logger.Error("Expected runtime Matchmaker Matched function but didn't find it.") - return "", false - } - - ctx := NewLuaContext(runtime.vm, runtime.luaEnv, ExecutionModeMatchmaker, nil, 0, "", "", "", "", "") - - entriesTable := runtime.vm.CreateTable(len(entries), 0) - for i, entry := range entries { - presenceTable := runtime.vm.CreateTable(0, 4) - presenceTable.RawSetString("user_id", lua.LString(entry.Presence.UserId)) - presenceTable.RawSetString("session_id", lua.LString(entry.Presence.SessionId)) - presenceTable.RawSetString("username", lua.LString(entry.Presence.Username)) - presenceTable.RawSetString("node", lua.LString(entry.Presence.Node)) - - propertiesTable := runtime.vm.CreateTable(0, len(entry.StringProperties)+len(entry.NumericProperties)) - for k, v := range entry.StringProperties { - propertiesTable.RawSetString(k, lua.LString(v)) - } - for k, v := range entry.NumericProperties { - propertiesTable.RawSetString(k, lua.LNumber(v)) - } - - entryTable := runtime.vm.CreateTable(0, 2) - entryTable.RawSetString("presence", presenceTable) - entryTable.RawSetString("properties", propertiesTable) - - entriesTable.RawSetInt(i+1, entryTable) - } - - retValue, err, _ := runtime.invokeFunction(runtime.vm, lf, ctx, entriesTable) - runtimePool.Put(runtime) - if err != nil { - logger.Error("Error running runtime Matchmaker Matched hook.", zap.Error(err)) - return "", false - } - - if retValue == nil || retValue == lua.LNil { - // No return value or hook decided not to return an authoritative match ID. - return "", false - } - - if retValue.Type() == lua.LTString { - // Hook (maybe) returned an authoritative match ID. - matchIDString := retValue.String() - - // Validate the match ID. - matchIDComponents := strings.SplitN(matchIDString, ".", 2) - if len(matchIDComponents) != 2 { - logger.Error("Invalid return value from runtime Matchmaker Matched hook, not a valid match ID.") - return "", false - } - _, err = uuid.FromString(matchIDComponents[0]) - if err != nil { - logger.Error("Invalid return value from runtime Matchmaker Matched hook, not a valid match ID.") - return "", false - } - - return matchIDString, true - } - - logger.Error("Unexpected return type from runtime Matchmaker Matched hook, must be string or nil.") - return "", false -} diff --git a/server/runtime_lua.go b/server/runtime_lua.go new file mode 100644 index 0000000000000000000000000000000000000000..0740e11bb01aa422708c9c14d8f3247f3653ee69 --- /dev/null +++ b/server/runtime_lua.go @@ -0,0 +1,1443 @@ +// Copyright 2018 The Nakama Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import ( + "bytes" + "context" + "database/sql" + "encoding/json" + "errors" + "fmt" + "github.com/gofrs/uuid" + "github.com/golang/protobuf/jsonpb" + "github.com/golang/protobuf/proto" + "github.com/golang/protobuf/ptypes/empty" + "github.com/heroiclabs/nakama/api" + "github.com/heroiclabs/nakama/rtapi" + "github.com/heroiclabs/nakama/social" + "github.com/yuin/gopher-lua" + "go.opencensus.io/stats" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "google.golang.org/grpc/codes" + "io/ioutil" + "os" + "path/filepath" + "sort" + "strings" + "sync" +) + +const LTSentinel = lua.LValueType(-1) + +type LSentinelType struct { + lua.LNilType +} + +func (s *LSentinelType) String() string { return "" } +func (s *LSentinelType) Type() lua.LValueType { return LTSentinel } + +var LSentinel = lua.LValue(&LSentinelType{}) + +type RuntimeLuaModule struct { + Name string + Path string + Content []byte +} + +type RuntimeLuaModuleCache struct { + Names []string + Modules map[string]*RuntimeLuaModule +} + +func (mc *RuntimeLuaModuleCache) Add(m *RuntimeLuaModule) { + mc.Names = append(mc.Names, m.Name) + mc.Modules[m.Name] = m + + // Ensure modules will be listed in ascending order of names. + sort.Strings(mc.Names) +} + +type RuntimeProviderLua struct { + sync.Mutex + logger *zap.Logger + db *sql.DB + jsonpbMarshaler *jsonpb.Marshaler + jsonpbUnmarshaler *jsonpb.Unmarshaler + config Config + socialClient *social.Client + leaderboardCache LeaderboardCache + sessionRegistry *SessionRegistry + matchRegistry MatchRegistry + tracker Tracker + router MessageRouter + stdLibs map[string]lua.LGFunction + + once *sync.Once + poolCh chan *RuntimeLua + maxCount int + currentCount int + newFn func() *RuntimeLua + + statsCtx context.Context +} + +func NewRuntimeProviderLua(logger, startupLogger *zap.Logger, db *sql.DB, jsonpbMarshaler *jsonpb.Marshaler, jsonpbUnmarshaler *jsonpb.Unmarshaler, config Config, socialClient *social.Client, leaderboardCache LeaderboardCache, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter, goMatchCreateFn RuntimeMatchCreateFunction, rootPath string, paths []string) ([]string, map[string]RuntimeRpcFunction, map[string]RuntimeBeforeRtFunction, map[string]RuntimeAfterRtFunction, *RuntimeBeforeReqFunctions, *RuntimeAfterReqFunctions, RuntimeMatchmakerMatchedFunction, RuntimeMatchCreateFunction, error) { + moduleCache := &RuntimeLuaModuleCache{ + Names: make([]string, 0), + Modules: make(map[string]*RuntimeLuaModule, 0), + } + modulePaths := make([]string, 0) + + // Override before Package library is invoked. + lua.LuaLDir = rootPath + lua.LuaPathDefault = lua.LuaLDir + string(os.PathSeparator) + "?.lua;" + lua.LuaLDir + string(os.PathSeparator) + "?" + string(os.PathSeparator) + "init.lua" + os.Setenv(lua.LuaPath, lua.LuaPathDefault) + + startupLogger.Info("Initialising Lua runtime provider", zap.String("path", lua.LuaLDir)) + + for _, path := range paths { + if strings.ToLower(filepath.Ext(path)) != ".lua" { + continue + } + + // Load the file contents into memory. + var content []byte + var err error + if content, err = ioutil.ReadFile(path); err != nil { + startupLogger.Error("Could not read Lua module", zap.String("path", path), zap.Error(err)) + return nil, nil, nil, nil, nil, nil, nil, nil, err + } + + relPath, _ := filepath.Rel(rootPath, path) + name := strings.TrimSuffix(relPath, filepath.Ext(relPath)) + // Make paths Lua friendly. + name = strings.Replace(name, string(os.PathSeparator), ".", -1) + + moduleCache.Add(&RuntimeLuaModule{ + Name: name, + Path: path, + Content: content, + }) + modulePaths = append(modulePaths, relPath) + } + + stdLibs := map[string]lua.LGFunction{ + lua.LoadLibName: OpenPackage(moduleCache), + lua.BaseLibName: lua.OpenBase, + lua.TabLibName: lua.OpenTable, + lua.OsLibName: OpenOs, + lua.StringLibName: lua.OpenString, + lua.MathLibName: lua.OpenMath, + Bit32LibName: OpenBit32, + } + once := &sync.Once{} + localCache := NewRuntimeLuaLocalCache() + rpcFunctions := make(map[string]RuntimeRpcFunction, 0) + beforeRtFunctions := make(map[string]RuntimeBeforeRtFunction, 0) + afterRtFunctions := make(map[string]RuntimeAfterRtFunction, 0) + beforeReqFunctions := &RuntimeBeforeReqFunctions{} + afterReqFunctions := &RuntimeAfterReqFunctions{} + var matchmakerMatchedFunction RuntimeMatchmakerMatchedFunction + + allMatchCreateFn := func(logger *zap.Logger, id uuid.UUID, node string, name string, labelUpdateFn func(string)) (RuntimeMatchCore, error) { + core, err := goMatchCreateFn(logger, id, node, name, labelUpdateFn) + if err != nil { + return nil, err + } + if core != nil { + return core, nil + } + return NewRuntimeLuaMatchCore(logger, db, config, socialClient, leaderboardCache, sessionRegistry, matchRegistry, tracker, router, stdLibs, once, localCache, goMatchCreateFn, id, node, name, labelUpdateFn) + } + + runtimeProviderLua := &RuntimeProviderLua{ + logger: logger, + db: db, + jsonpbMarshaler: jsonpbMarshaler, + jsonpbUnmarshaler: jsonpbUnmarshaler, + config: config, + socialClient: socialClient, + leaderboardCache: leaderboardCache, + sessionRegistry: sessionRegistry, + matchRegistry: matchRegistry, + tracker: tracker, + router: router, + stdLibs: stdLibs, + + once: once, + poolCh: make(chan *RuntimeLua, config.GetRuntime().MaxCount), + maxCount: config.GetRuntime().MaxCount, + // Set the current count assuming we'll warm up the pool in a moment. + currentCount: config.GetRuntime().MinCount, + newFn: func() *RuntimeLua { + r, err := newRuntimeLuaVM(logger, db, config, socialClient, leaderboardCache, sessionRegistry, matchRegistry, tracker, router, stdLibs, moduleCache, once, localCache, allMatchCreateFn, nil) + if err != nil { + logger.Fatal("Failed to initialize Lua runtime", zap.Error(err)) + } + return r + }, + + statsCtx: context.Background(), + } + + startupLogger.Info("Evaluating Lua runtime modules") + + r, err := newRuntimeLuaVM(logger, db, config, socialClient, leaderboardCache, sessionRegistry, matchRegistry, tracker, router, stdLibs, moduleCache, once, localCache, allMatchCreateFn, func(execMode RuntimeExecutionMode, id string) { + switch execMode { + case RuntimeExecutionModeRPC: + rpcFunctions[id] = func(queryParams map[string][]string, userID, username string, expiry int64, sessionID, clientIP, clientPort, payload string) (string, error, codes.Code) { + return runtimeProviderLua.Rpc(id, queryParams, userID, username, expiry, sessionID, clientIP, clientPort, payload) + } + case RuntimeExecutionModeBefore: + if strings.HasPrefix(id, strings.ToLower(RTAPI_PREFIX)) { + beforeRtFunctions[id] = func(logger *zap.Logger, userID, username string, expiry int64, sessionID, clientIP, clientPort string, envelope *rtapi.Envelope) (*rtapi.Envelope, error) { + return runtimeProviderLua.BeforeRt(id, logger, userID, username, expiry, sessionID, clientIP, clientPort, envelope) + } + } else if strings.HasPrefix(id, strings.ToLower(API_PREFIX)) { + shortId := strings.TrimPrefix(id, strings.ToLower(API_PREFIX)) + switch shortId { + case "getaccount": + beforeReqFunctions.beforeGetAccountFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) (*empty.Empty, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*empty.Empty), nil, 0 + } + case "updateaccount": + beforeReqFunctions.beforeUpdateAccountFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.UpdateAccountRequest) (*api.UpdateAccountRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.UpdateAccountRequest), nil, 0 + } + case "authenticatecustom": + beforeReqFunctions.beforeAuthenticateCustomFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AuthenticateCustomRequest) (*api.AuthenticateCustomRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.AuthenticateCustomRequest), nil, 0 + } + case "authenticatedevice": + beforeReqFunctions.beforeAuthenticateDeviceFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AuthenticateDeviceRequest) (*api.AuthenticateDeviceRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.AuthenticateDeviceRequest), nil, 0 + } + case "authenticateemail": + beforeReqFunctions.beforeAuthenticateEmailFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AuthenticateEmailRequest) (*api.AuthenticateEmailRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.AuthenticateEmailRequest), nil, 0 + } + case "authenticatefacebook": + beforeReqFunctions.beforeAuthenticateFacebookFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AuthenticateFacebookRequest) (*api.AuthenticateFacebookRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.AuthenticateFacebookRequest), nil, 0 + } + case "authenticategamecenter": + beforeReqFunctions.beforeAuthenticateGameCenterFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AuthenticateGameCenterRequest) (*api.AuthenticateGameCenterRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.AuthenticateGameCenterRequest), nil, 0 + } + case "authenticategoogle": + beforeReqFunctions.beforeAuthenticateGoogleFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AuthenticateGoogleRequest) (*api.AuthenticateGoogleRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.AuthenticateGoogleRequest), nil, 0 + } + case "authenticatesteam": + beforeReqFunctions.beforeAuthenticateSteamFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AuthenticateSteamRequest) (*api.AuthenticateSteamRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.AuthenticateSteamRequest), nil, 0 + } + case "listchannelmessages": + beforeReqFunctions.beforeListChannelMessagesFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ListChannelMessagesRequest) (*api.ListChannelMessagesRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.ListChannelMessagesRequest), nil, 0 + } + case "listfriends": + beforeReqFunctions.beforeListFriendsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *empty.Empty) (*empty.Empty, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*empty.Empty), nil, 0 + } + case "addfriends": + beforeReqFunctions.beforeAddFriendsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AddFriendsRequest) (*api.AddFriendsRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.AddFriendsRequest), nil, 0 + } + case "deletefriends": + beforeReqFunctions.beforeDeleteFriendsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.DeleteFriendsRequest) (*api.DeleteFriendsRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.DeleteFriendsRequest), nil, 0 + } + case "blockfriends": + beforeReqFunctions.beforeBlockFriendsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.BlockFriendsRequest) (*api.BlockFriendsRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.BlockFriendsRequest), nil, 0 + } + case "importfacebookfriends": + beforeReqFunctions.beforeImportFacebookFriendsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ImportFacebookFriendsRequest) (*api.ImportFacebookFriendsRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.ImportFacebookFriendsRequest), nil, 0 + } + case "creategroup": + beforeReqFunctions.beforeCreateGroupFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.CreateGroupRequest) (*api.CreateGroupRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.CreateGroupRequest), nil, 0 + } + case "updategroup": + beforeReqFunctions.beforeUpdateGroupFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.UpdateGroupRequest) (*api.UpdateGroupRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.UpdateGroupRequest), nil, 0 + } + case "deletegroup": + beforeReqFunctions.beforeDeleteGroupFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.DeleteGroupRequest) (*api.DeleteGroupRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.DeleteGroupRequest), nil, 0 + } + case "joingroup": + beforeReqFunctions.beforeJoinGroupFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.JoinGroupRequest) (*api.JoinGroupRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.JoinGroupRequest), nil, 0 + } + case "leavegroup": + beforeReqFunctions.beforeLeaveGroupFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.LeaveGroupRequest) (*api.LeaveGroupRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.LeaveGroupRequest), nil, 0 + } + case "addgroupusers": + beforeReqFunctions.beforeAddGroupUsersFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AddGroupUsersRequest) (*api.AddGroupUsersRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.AddGroupUsersRequest), nil, 0 + } + case "kickgroupusers": + beforeReqFunctions.beforeKickGroupUsersFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.KickGroupUsersRequest) (*api.KickGroupUsersRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.KickGroupUsersRequest), nil, 0 + } + case "promotegroupusers": + beforeReqFunctions.beforePromoteGroupUsersFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.PromoteGroupUsersRequest) (*api.PromoteGroupUsersRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.PromoteGroupUsersRequest), nil, 0 + } + case "listgroupusers": + beforeReqFunctions.beforeListGroupUsersFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ListGroupUsersRequest) (*api.ListGroupUsersRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.ListGroupUsersRequest), nil, 0 + } + case "listusergroups": + beforeReqFunctions.beforeListUserGroupsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ListUserGroupsRequest) (*api.ListUserGroupsRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.ListUserGroupsRequest), nil, 0 + } + case "listgroups": + beforeReqFunctions.beforeListGroupsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ListGroupsRequest) (*api.ListGroupsRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.ListGroupsRequest), nil, 0 + } + case "deleteleaderboardrecord": + beforeReqFunctions.beforeDeleteLeaderboardRecordFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.DeleteLeaderboardRecordRequest) (*api.DeleteLeaderboardRecordRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.DeleteLeaderboardRecordRequest), nil, 0 + } + case "listleaderboardrecords": + beforeReqFunctions.beforeListLeaderboardRecordsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ListLeaderboardRecordsRequest) (*api.ListLeaderboardRecordsRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.ListLeaderboardRecordsRequest), nil, 0 + } + case "writeleaderboardrecord": + beforeReqFunctions.beforeWriteLeaderboardRecordFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.WriteLeaderboardRecordRequest) (*api.WriteLeaderboardRecordRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.WriteLeaderboardRecordRequest), nil, 0 + } + case "linkcustom": + beforeReqFunctions.beforeLinkCustomFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountCustom) (*api.AccountCustom, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.AccountCustom), nil, 0 + } + case "linkdevice": + beforeReqFunctions.beforeLinkDeviceFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountDevice) (*api.AccountDevice, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.AccountDevice), nil, 0 + } + case "linkemail": + beforeReqFunctions.beforeLinkEmailFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountEmail) (*api.AccountEmail, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.AccountEmail), nil, 0 + } + case "linkfacebook": + beforeReqFunctions.beforeLinkFacebookFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.LinkFacebookRequest) (*api.LinkFacebookRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.LinkFacebookRequest), nil, 0 + } + case "linkgamecenter": + beforeReqFunctions.beforeLinkGameCenterFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountGameCenter) (*api.AccountGameCenter, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.AccountGameCenter), nil, 0 + } + case "linkgoogle": + beforeReqFunctions.beforeLinkGoogleFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountGoogle) (*api.AccountGoogle, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.AccountGoogle), nil, 0 + } + case "linksteam": + beforeReqFunctions.beforeLinkSteamFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountSteam) (*api.AccountSteam, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.AccountSteam), nil, 0 + } + case "listmatches": + beforeReqFunctions.beforeListMatchesFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ListMatchesRequest) (*api.ListMatchesRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.ListMatchesRequest), nil, 0 + } + case "listnotifications": + beforeReqFunctions.beforeListNotificationsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ListNotificationsRequest) (*api.ListNotificationsRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.ListNotificationsRequest), nil, 0 + } + case "deletenotification": + beforeReqFunctions.beforeDeleteNotificationFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.DeleteNotificationsRequest) (*api.DeleteNotificationsRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.DeleteNotificationsRequest), nil, 0 + } + case "liststorageobjects": + beforeReqFunctions.beforeListStorageObjectsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ListStorageObjectsRequest) (*api.ListStorageObjectsRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.ListStorageObjectsRequest), nil, 0 + } + case "readstorageobjects": + beforeReqFunctions.beforeReadStorageObjectsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.ReadStorageObjectsRequest) (*api.ReadStorageObjectsRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.ReadStorageObjectsRequest), nil, 0 + } + case "writestorageobjects": + beforeReqFunctions.beforeWriteStorageObjectsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.WriteStorageObjectsRequest) (*api.WriteStorageObjectsRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.WriteStorageObjectsRequest), nil, 0 + } + case "deletestorageobjects": + beforeReqFunctions.beforeDeleteStorageObjectsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.DeleteStorageObjectsRequest) (*api.DeleteStorageObjectsRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.DeleteStorageObjectsRequest), nil, 0 + } + case "unlinkcustom": + beforeReqFunctions.beforeUnlinkCustomFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountCustom) (*api.AccountCustom, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.AccountCustom), nil, 0 + } + case "unlinkdevice": + beforeReqFunctions.beforeUnlinkDeviceFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountDevice) (*api.AccountDevice, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.AccountDevice), nil, 0 + } + case "unlinkemail": + beforeReqFunctions.beforeUnlinkEmailFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountEmail) (*api.AccountEmail, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.AccountEmail), nil, 0 + } + case "unlinkfacebook": + beforeReqFunctions.beforeUnlinkFacebookFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountFacebook) (*api.AccountFacebook, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.AccountFacebook), nil, 0 + } + case "unlinkgamecenter": + beforeReqFunctions.beforeUnlinkGameCenterFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountGameCenter) (*api.AccountGameCenter, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.AccountGameCenter), nil, 0 + } + case "unlinkgoogle": + beforeReqFunctions.beforeUnlinkGoogleFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountGoogle) (*api.AccountGoogle, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.AccountGoogle), nil, 0 + } + case "unlinksteam": + beforeReqFunctions.beforeUnlinkSteamFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.AccountSteam) (*api.AccountSteam, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.AccountSteam), nil, 0 + } + case "getusers": + beforeReqFunctions.beforeGetUsersFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, in *api.GetUsersRequest) (*api.GetUsersRequest, error, codes.Code) { + result, err, code := runtimeProviderLua.BeforeReq(id, logger, userID, username, expiry, clientIP, clientPort, in) + if result == nil || err != nil { + return nil, err, code + } + return result.(*api.GetUsersRequest), nil, 0 + } + } + } + case RuntimeExecutionModeAfter: + if strings.HasPrefix(id, strings.ToLower(RTAPI_PREFIX)) { + afterRtFunctions[id] = func(logger *zap.Logger, userID, username string, expiry int64, sessionID, clientIP, clientPort string, envelope *rtapi.Envelope) error { + return runtimeProviderLua.AfterRt(id, logger, userID, username, expiry, sessionID, clientIP, clientPort, envelope) + } + } else if strings.HasPrefix(id, strings.ToLower(API_PREFIX)) { + shortId := strings.TrimPrefix(id, strings.ToLower(API_PREFIX)) + switch shortId { + case "getaccount": + afterReqFunctions.afterGetAccountFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.Account) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "updateaccount": + afterReqFunctions.afterUpdateAccountFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "authenticatecustom": + afterReqFunctions.afterAuthenticateCustomFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.Session) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "authenticatedevice": + afterReqFunctions.afterAuthenticateDeviceFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.Session) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "authenticateemail": + afterReqFunctions.afterAuthenticateEmailFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.Session) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "authenticatefacebook": + afterReqFunctions.afterAuthenticateFacebookFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.Session) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "authenticategamecenter": + afterReqFunctions.afterAuthenticateGameCenterFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.Session) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "authenticategoogle": + afterReqFunctions.afterAuthenticateGoogleFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.Session) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "authenticatesteam": + afterReqFunctions.afterAuthenticateSteamFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.Session) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "listchannelmessages": + afterReqFunctions.afterListChannelMessagesFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.ChannelMessageList) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "listfriends": + afterReqFunctions.afterListFriendsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.Friends) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "addfriends": + afterReqFunctions.afterAddFriendsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "deletefriends": + afterReqFunctions.afterDeleteFriendsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "blockfriends": + afterReqFunctions.afterBlockFriendsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "importfacebookfriends": + afterReqFunctions.afterImportFacebookFriendsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "creategroup": + afterReqFunctions.afterCreateGroupFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.Group) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "updategroup": + afterReqFunctions.afterUpdateGroupFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "deletegroup": + afterReqFunctions.afterDeleteGroupFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "joingroup": + afterReqFunctions.afterJoinGroupFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "leavegroup": + afterReqFunctions.afterLeaveGroupFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "addgroupusers": + afterReqFunctions.afterAddGroupUsersFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "kickgroupusers": + afterReqFunctions.afterKickGroupUsersFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "promotegroupusers": + afterReqFunctions.afterPromoteGroupUsersFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "listgroupusers": + afterReqFunctions.afterListGroupUsersFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.GroupUserList) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "listusergroups": + afterReqFunctions.afterListUserGroupsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.UserGroupList) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "listgroups": + afterReqFunctions.afterListGroupsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.GroupList) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "deleteleaderboardrecord": + afterReqFunctions.afterDeleteLeaderboardRecordFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "listleaderboardrecords": + afterReqFunctions.afterListLeaderboardRecordsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.LeaderboardRecordList) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "writeleaderboardrecord": + afterReqFunctions.afterWriteLeaderboardRecordFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.LeaderboardRecord) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "linkcustom": + afterReqFunctions.afterLinkCustomFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "linkdevice": + afterReqFunctions.afterLinkDeviceFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "linkemail": + afterReqFunctions.afterLinkEmailFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "linkfacebook": + afterReqFunctions.afterLinkFacebookFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "linkgamecenter": + afterReqFunctions.afterLinkGameCenterFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "linkgoogle": + afterReqFunctions.afterLinkGoogleFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "linksteam": + afterReqFunctions.afterLinkSteamFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "listmatches": + afterReqFunctions.afterListMatchesFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.MatchList) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "listnotifications": + afterReqFunctions.afterListNotificationsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.NotificationList) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "deletenotification": + afterReqFunctions.afterDeleteNotificationFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "liststorageobjects": + afterReqFunctions.afterListStorageObjectsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.StorageObjectList) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "readstorageobjects": + afterReqFunctions.afterReadStorageObjectsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.StorageObjects) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "writestorageobjects": + afterReqFunctions.afterWriteStorageObjectsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.StorageObjectAcks) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "deletestorageobjects": + afterReqFunctions.afterDeleteStorageObjectsFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "unlinkcustom": + afterReqFunctions.afterUnlinkCustomFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "unlinkdevice": + afterReqFunctions.afterUnlinkDeviceFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "unlinkemail": + afterReqFunctions.afterUnlinkEmailFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "unlinkfacebook": + afterReqFunctions.afterUnlinkFacebookFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "unlinkgamecenter": + afterReqFunctions.afterUnlinkGameCenterFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "unlinkgoogle": + afterReqFunctions.afterUnlinkGoogleFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "unlinksteam": + afterReqFunctions.afterUnlinkSteamFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *empty.Empty) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + case "getusers": + afterReqFunctions.afterGetUsersFunction = func(logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, out *api.Users) error { + return runtimeProviderLua.AfterReq(id, logger, userID, username, expiry, clientIP, clientPort, out) + } + } + } + case RuntimeExecutionModeMatchmaker: + matchmakerMatchedFunction = func(entries []*MatchmakerEntry) (string, bool, error) { + return runtimeProviderLua.MatchmakerMatched(entries) + } + } + }) + if err != nil { + return nil, nil, nil, nil, nil, nil, nil, nil, err + } + r.Stop() + + startupLogger.Info("Lua runtime modules loaded") + + // Warm up the pool. + startupLogger.Info("Allocating minimum runtime pool", zap.Int("count", runtimeProviderLua.currentCount)) + if len(moduleCache.Names) > 0 { + // Only if there are runtime modules to load. + for i := 0; i < config.GetRuntime().MinCount; i++ { + runtimeProviderLua.poolCh <- runtimeProviderLua.newFn() + } + stats.Record(runtimeProviderLua.statsCtx, MetricsRuntimeCount.M(int64(config.GetRuntime().MinCount))) + } + startupLogger.Info("Allocated minimum runtime pool") + + return modulePaths, rpcFunctions, beforeRtFunctions, afterRtFunctions, beforeReqFunctions, afterReqFunctions, matchmakerMatchedFunction, allMatchCreateFn, nil +} + +func (rp *RuntimeProviderLua) Rpc(id string, queryParams map[string][]string, userID, username string, expiry int64, sessionID, clientIP, clientPort, payload string) (string, error, codes.Code) { + runtime := rp.Get() + lf := runtime.GetCallback(RuntimeExecutionModeRPC, id) + if lf == nil { + rp.Put(runtime) + return "", ErrRuntimeRPCNotFound, codes.NotFound + } + + result, fnErr, code := runtime.InvokeFunction(RuntimeExecutionModeRPC, lf, queryParams, userID, username, expiry, sessionID, clientIP, clientPort, payload) + rp.Put(runtime) + + if fnErr != nil { + rp.logger.Error("Runtime RPC function caused an error", zap.String("id", id), zap.Error(fnErr)) + if apiErr, ok := fnErr.(*lua.ApiError); ok && !rp.logger.Core().Enabled(zapcore.InfoLevel) { + msg := apiErr.Object.String() + if strings.HasPrefix(msg, lf.Proto.SourceName) { + msg = msg[len(lf.Proto.SourceName):] + msgParts := strings.SplitN(msg, ": ", 2) + if len(msgParts) == 2 { + msg = msgParts[1] + } else { + msg = msgParts[0] + } + } + return "", errors.New(msg), code + } else { + return "", fnErr, code + } + } + + if result == nil { + return "", nil, 0 + } + + if payload, ok := result.(string); !ok { + rp.logger.Warn("Lua runtime function returned invalid data", zap.Any("result", result)) + return "", errors.New("Runtime function returned invalid data - only allowed one return value of type String/Byte."), codes.Internal + } else { + return payload, nil, 0 + } +} + +func (rp *RuntimeProviderLua) BeforeRt(id string, logger *zap.Logger, userID, username string, expiry int64, sessionID, clientIP, clientPort string, envelope *rtapi.Envelope) (*rtapi.Envelope, error) { + runtime := rp.Get() + lf := runtime.GetCallback(RuntimeExecutionModeBefore, id) + if lf == nil { + rp.Put(runtime) + return nil, errors.New("Runtime Before function not found.") + } + + envelopeJSON, err := rp.jsonpbMarshaler.MarshalToString(envelope) + if err != nil { + rp.Put(runtime) + logger.Error("Could not marshall envelope to JSON", zap.Any("envelope", envelope), zap.Error(err)) + return nil, errors.New("Could not run runtime Before function.") + } + var envelopeMap map[string]interface{} + if err := json.Unmarshal([]byte(envelopeJSON), &envelopeMap); err != nil { + rp.Put(runtime) + logger.Error("Could not unmarshall envelope to interface{}", zap.Any("envelope_json", envelopeJSON), zap.Error(err)) + return nil, errors.New("Could not run runtime Before function.") + } + + result, fnErr, _ := runtime.InvokeFunction(RuntimeExecutionModeBefore, lf, nil, userID, username, expiry, sessionID, clientIP, clientPort, envelopeMap) + rp.Put(runtime) + + if fnErr != nil { + logger.Error("Runtime Before function caused an error.", zap.String("id", id), zap.Error(fnErr)) + if apiErr, ok := fnErr.(*lua.ApiError); ok && !logger.Core().Enabled(zapcore.InfoLevel) { + msg := apiErr.Object.String() + if strings.HasPrefix(msg, lf.Proto.SourceName) { + msg = msg[len(lf.Proto.SourceName):] + msgParts := strings.SplitN(msg, ": ", 2) + if len(msgParts) == 2 { + msg = msgParts[1] + } else { + msg = msgParts[0] + } + } + return nil, errors.New(msg) + } else { + return nil, fnErr + } + } + + if result == nil { + return nil, nil + } + + resultJSON, err := json.Marshal(result) + if err != nil { + logger.Error("Could not marshall result to JSON", zap.Any("result", result), zap.Error(err)) + return nil, errors.New("Could not complete runtime Before function.") + } + + if err = rp.jsonpbUnmarshaler.Unmarshal(strings.NewReader(string(resultJSON)), envelope); err != nil { + logger.Error("Could not unmarshall result to envelope", zap.Any("result", result), zap.Error(err)) + return nil, errors.New("Could not complete runtime Before function.") + } + + return envelope, nil +} + +func (rp *RuntimeProviderLua) AfterRt(id string, logger *zap.Logger, userID, username string, expiry int64, sessionID, clientIP, clientPort string, envelope *rtapi.Envelope) error { + runtime := rp.Get() + lf := runtime.GetCallback(RuntimeExecutionModeAfter, id) + if lf == nil { + rp.Put(runtime) + return errors.New("Runtime After function not found.") + } + + envelopeJSON, err := rp.jsonpbMarshaler.MarshalToString(envelope) + if err != nil { + rp.Put(runtime) + logger.Error("Could not marshall envelope to JSON", zap.Any("envelope", envelope), zap.Error(err)) + return errors.New("Could not run runtime After function.") + } + var envelopeMap map[string]interface{} + if err := json.Unmarshal([]byte(envelopeJSON), &envelopeMap); err != nil { + rp.Put(runtime) + logger.Error("Could not unmarshall envelope to interface{}", zap.Any("envelope_json", envelopeJSON), zap.Error(err)) + return errors.New("Could not run runtime After function.") + } + + _, fnErr, _ := runtime.InvokeFunction(RuntimeExecutionModeAfter, lf, nil, userID, username, expiry, sessionID, clientIP, clientPort, envelopeMap) + rp.Put(runtime) + + if fnErr != nil { + logger.Error("Runtime After function caused an error.", zap.String("id", id), zap.Error(fnErr)) + if apiErr, ok := fnErr.(*lua.ApiError); ok && !logger.Core().Enabled(zapcore.InfoLevel) { + msg := apiErr.Object.String() + if strings.HasPrefix(msg, lf.Proto.SourceName) { + msg = msg[len(lf.Proto.SourceName):] + msgParts := strings.SplitN(msg, ": ", 2) + if len(msgParts) == 2 { + msg = msgParts[1] + } else { + msg = msgParts[0] + } + } + return errors.New(msg) + } else { + return fnErr + } + } + + return nil +} + +func (rp *RuntimeProviderLua) BeforeReq(id string, logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, req interface{}) (interface{}, error, codes.Code) { + runtime := rp.Get() + lf := runtime.GetCallback(RuntimeExecutionModeBefore, id) + if lf == nil { + rp.Put(runtime) + return nil, errors.New("Runtime Before function not found."), codes.NotFound + } + + reqProto, ok := req.(proto.Message) + if !ok { + rp.Put(runtime) + logger.Error("Could not cast request to message", zap.Any("request", req)) + return nil, errors.New("Could not run runtime Before function."), codes.Internal + } + reqJSON, err := rp.jsonpbMarshaler.MarshalToString(reqProto) + if err != nil { + rp.Put(runtime) + logger.Error("Could not marshall request to JSON", zap.Any("request", reqProto), zap.Error(err)) + return nil, errors.New("Could not run runtime Before function."), codes.Internal + } + var reqMap map[string]interface{} + if err := json.Unmarshal([]byte(reqJSON), &reqMap); err != nil { + rp.Put(runtime) + logger.Error("Could not unmarshall request to interface{}", zap.Any("request_json", reqJSON), zap.Error(err)) + return nil, errors.New("Could not run runtime Before function."), codes.Internal + } + + result, fnErr, code := runtime.InvokeFunction(RuntimeExecutionModeBefore, lf, nil, userID, username, expiry, "", clientIP, clientPort, reqMap) + rp.Put(runtime) + + if fnErr != nil { + logger.Error("Runtime Before function caused an error.", zap.String("id", id), zap.Error(fnErr)) + if apiErr, ok := fnErr.(*lua.ApiError); ok && !logger.Core().Enabled(zapcore.InfoLevel) { + msg := apiErr.Object.String() + if strings.HasPrefix(msg, lf.Proto.SourceName) { + msg = msg[len(lf.Proto.SourceName):] + msgParts := strings.SplitN(msg, ": ", 2) + if len(msgParts) == 2 { + msg = msgParts[1] + } else { + msg = msgParts[0] + } + } + return nil, errors.New(msg), code + } else { + return nil, fnErr, code + } + } + + if result == nil { + return nil, nil, 0 + } + + resultJSON, err := json.Marshal(result) + if err != nil { + logger.Error("Could not marshall result to JSON", zap.Any("result", result), zap.Error(err)) + return nil, errors.New("Could not complete runtime Before function."), codes.Internal + } + + if err = rp.jsonpbUnmarshaler.Unmarshal(strings.NewReader(string(resultJSON)), reqProto); err != nil { + logger.Error("Could not unmarshall result to request", zap.Any("result", result), zap.Error(err)) + return nil, errors.New("Could not complete runtime Before function."), codes.Internal + } + + return req, nil, 0 +} + +func (rp *RuntimeProviderLua) AfterReq(id string, logger *zap.Logger, userID, username string, expiry int64, clientIP, clientPort string, req interface{}) error { + runtime := rp.Get() + lf := runtime.GetCallback(RuntimeExecutionModeAfter, id) + if lf == nil { + rp.Put(runtime) + return errors.New("Runtime After function not found.") + } + + reqProto, ok := req.(proto.Message) + if !ok { + rp.Put(runtime) + logger.Error("Could not cast request to message", zap.Any("request", req)) + return errors.New("Could not run runtime After function.") + } + reqJSON, err := rp.jsonpbMarshaler.MarshalToString(reqProto) + if err != nil { + rp.Put(runtime) + logger.Error("Could not marshall request to JSON", zap.Any("request", reqProto), zap.Error(err)) + return errors.New("Could not run runtime After function.") + } + var reqMap map[string]interface{} + if err := json.Unmarshal([]byte(reqJSON), &reqMap); err != nil { + rp.Put(runtime) + logger.Error("Could not unmarshall request to interface{}", zap.Any("request_json", reqJSON), zap.Error(err)) + return errors.New("Could not run runtime After function.") + } + + _, fnErr, _ := runtime.InvokeFunction(RuntimeExecutionModeBefore, lf, nil, userID, username, expiry, "", clientIP, clientPort, reqMap) + rp.Put(runtime) + + if fnErr != nil { + logger.Error("Runtime After function caused an error.", zap.String("id", id), zap.Error(fnErr)) + if apiErr, ok := fnErr.(*lua.ApiError); ok && !logger.Core().Enabled(zapcore.InfoLevel) { + msg := apiErr.Object.String() + if strings.HasPrefix(msg, lf.Proto.SourceName) { + msg = msg[len(lf.Proto.SourceName):] + msgParts := strings.SplitN(msg, ": ", 2) + if len(msgParts) == 2 { + msg = msgParts[1] + } else { + msg = msgParts[0] + } + } + return errors.New(msg) + } else { + return fnErr + } + } + + return nil +} + +func (rp *RuntimeProviderLua) MatchmakerMatched(entries []*MatchmakerEntry) (string, bool, error) { + runtime := rp.Get() + lf := runtime.GetCallback(RuntimeExecutionModeMatchmaker, "") + if lf == nil { + rp.Put(runtime) + return "", false, errors.New("Runtime Matchmaker Matched function not found.") + } + + ctx := NewRuntimeLuaContext(runtime.vm, runtime.luaEnv, RuntimeExecutionModeMatchmaker, nil, 0, "", "", "", "", "") + + entriesTable := runtime.vm.CreateTable(len(entries), 0) + for i, entry := range entries { + presenceTable := runtime.vm.CreateTable(0, 4) + presenceTable.RawSetString("user_id", lua.LString(entry.Presence.UserId)) + presenceTable.RawSetString("session_id", lua.LString(entry.Presence.SessionId)) + presenceTable.RawSetString("username", lua.LString(entry.Presence.Username)) + presenceTable.RawSetString("node", lua.LString(entry.Presence.Node)) + + propertiesTable := runtime.vm.CreateTable(0, len(entry.StringProperties)+len(entry.NumericProperties)) + for k, v := range entry.StringProperties { + propertiesTable.RawSetString(k, lua.LString(v)) + } + for k, v := range entry.NumericProperties { + propertiesTable.RawSetString(k, lua.LNumber(v)) + } + + entryTable := runtime.vm.CreateTable(0, 2) + entryTable.RawSetString("presence", presenceTable) + entryTable.RawSetString("properties", propertiesTable) + + entriesTable.RawSetInt(i+1, entryTable) + } + + retValue, err, _ := runtime.invokeFunction(runtime.vm, lf, ctx, entriesTable) + rp.Put(runtime) + if err != nil { + return "", false, fmt.Errorf("Error running runtime Matchmaker Matched hook: %v", err.Error()) + } + + if retValue == nil || retValue == lua.LNil { + // No return value or hook decided not to return an authoritative match ID. + return "", false, nil + } + + if retValue.Type() == lua.LTString { + // Hook (maybe) returned an authoritative match ID. + matchIDString := retValue.String() + + // Validate the match ID. + matchIDComponents := strings.SplitN(matchIDString, ".", 2) + if len(matchIDComponents) != 2 { + return "", false, errors.New("Invalid return value from runtime Matchmaker Matched hook, not a valid match ID.") + } + _, err = uuid.FromString(matchIDComponents[0]) + if err != nil { + return "", false, errors.New("Invalid return value from runtime Matchmaker Matched hook, not a valid match ID.") + } + + return matchIDString, true, nil + } + + return "", false, errors.New("Unexpected return type from runtime Matchmaker Matched hook, must be string or nil.") +} + +func (rp *RuntimeProviderLua) Get() *RuntimeLua { + select { + case r := <-rp.poolCh: + // Ideally use an available idle runtime. + return r + default: + // If there was no idle runtime, see if we can allocate a new one. + rp.Lock() + if rp.currentCount >= rp.maxCount { + rp.Unlock() + // If we've reached the max allowed allocation block on an available runtime. + return <-rp.poolCh + } + // Inside the locked region now, last chance to use an available idle runtime. + // Note: useful in case a runtime becomes available while waiting to acquire lock. + select { + case r := <-rp.poolCh: + rp.Unlock() + return r + default: + // Allocate a new runtime. + rp.currentCount++ + rp.Unlock() + stats.Record(rp.statsCtx, MetricsRuntimeCount.M(1)) + return rp.newFn() + } + } +} + +func (rp *RuntimeProviderLua) Put(r *RuntimeLua) { + select { + case rp.poolCh <- r: + // Runtime is successfully returned to the pool. + default: + // The pool is over capacity. Should never happen but guard anyway. + // Safe to continue processing, the runtime is just discarded. + rp.logger.Warn("Runtime pool full, discarding Lua runtime") + } +} + +type RuntimeLua struct { + logger *zap.Logger + vm *lua.LState + luaEnv *lua.LTable +} + +func (r *RuntimeLua) loadModules(moduleCache *RuntimeLuaModuleCache) error { + // `DoFile(..)` only parses and evaluates modules. Calling it multiple times, will load and eval the file multiple times. + // So to make sure that we only load and evaluate modules once, regardless of whether there is dependency between files, we load them all into `preload`. + // This is to make sure that modules are only loaded and evaluated once as `doFile()` does not (always) update _LOADED table. + // Bear in mind two separate thoughts around the script runtime design choice: + // + // 1) This is only a problem if one module is dependent on another module. + // This means that the global functions are evaluated once at system startup and then later on when the module is required through `require`. + // We circumvent this by checking the _LOADED table to check if `require` had evaluated the module and avoiding double-eval. + // + // 2) Second item is that modules must be pre-loaded into the state for callback-func eval to work properly (in case of HTTP/RPC/etc invokes) + // So we need to always load the modules into the system via `preload` so that they are always available in the LState. + // We can't rely on `require` to have seen the module in case there is no dependency between the modules. + + //for _, mod := range r.modules { + // relPath, _ := filepath.Rel(r.luaPath, mod) + // moduleName := strings.TrimSuffix(relPath, filepath.Ext(relPath)) + // + // // check to see if this module was loaded by `require` before executing it + // loaded := l.GetField(l.Get(lua.RegistryIndex), "_LOADED") + // lv := l.GetField(loaded, moduleName) + // if lua.LVAsBool(lv) { + // // Already evaluated module via `require(..)` + // continue + // } + // + // if err = l.DoFile(mod); err != nil { + // failedModules++ + // r.logger.Error("Failed to evaluate module - skipping", zap.String("path", mod), zap.Error(err)) + // } + //} + + preload := r.vm.GetField(r.vm.GetField(r.vm.Get(lua.EnvironIndex), "package"), "preload") + fns := make(map[string]*lua.LFunction) + for _, name := range moduleCache.Names { + module, ok := moduleCache.Modules[name] + if !ok { + r.logger.Fatal("Failed to find named module in cache", zap.String("name", name)) + } + f, err := r.vm.Load(bytes.NewReader(module.Content), module.Path) + if err != nil { + r.logger.Error("Could not load module", zap.String("name", module.Path), zap.Error(err)) + return err + } else { + r.vm.SetField(preload, module.Name, f) + fns[module.Name] = f + } + } + + for _, name := range moduleCache.Names { + fn, ok := fns[name] + if !ok { + r.logger.Fatal("Failed to find named module in prepared functions", zap.String("name", name)) + } + loaded := r.vm.GetField(r.vm.Get(lua.RegistryIndex), "_LOADED") + lv := r.vm.GetField(loaded, name) + if lua.LVAsBool(lv) { + // Already evaluated module via `require(..)` + continue + } + + r.vm.Push(fn) + fnErr := r.vm.PCall(0, -1, nil) + if fnErr != nil { + r.logger.Error("Could not complete runtime invocation", zap.Error(fnErr)) + return fnErr + } + } + + return nil +} + +func (r *RuntimeLua) NewStateThread() (*lua.LState, context.CancelFunc) { + return r.vm, nil +} + +func (r *RuntimeLua) GetCallback(e RuntimeExecutionMode, key string) *lua.LFunction { + cp := r.vm.Context().Value(RUNTIME_LUA_CALLBACKS).(*RuntimeLuaCallbacks) + switch e { + case RuntimeExecutionModeRPC: + return cp.RPC[key] + case RuntimeExecutionModeBefore: + return cp.Before[key] + case RuntimeExecutionModeAfter: + return cp.After[key] + case RuntimeExecutionModeMatchmaker: + return cp.Matchmaker + } + + return nil +} + +func (r *RuntimeLua) InvokeFunction(execMode RuntimeExecutionMode, fn *lua.LFunction, queryParams map[string][]string, uid string, username string, sessionExpiry int64, sid string, clientIP string, clientPort string, payload interface{}) (interface{}, error, codes.Code) { + ctx := NewRuntimeLuaContext(r.vm, r.luaEnv, execMode, queryParams, sessionExpiry, uid, username, sid, clientIP, clientPort) + var lv lua.LValue + if payload != nil { + lv = RuntimeLuaConvertValue(r.vm, payload) + } + + retValue, err, code := r.invokeFunction(r.vm, fn, ctx, lv) + if err != nil { + return nil, err, code + } + + if retValue == nil || retValue == lua.LNil { + return nil, nil, 0 + } + + return RuntimeLuaConvertLuaValue(retValue), nil, 0 +} + +func (r *RuntimeLua) invokeFunction(l *lua.LState, fn *lua.LFunction, ctx *lua.LTable, payload lua.LValue) (lua.LValue, error, codes.Code) { + l.Push(LSentinel) + l.Push(fn) + + nargs := 1 + l.Push(ctx) + + if payload != nil { + nargs = 2 + l.Push(payload) + } + + err := l.PCall(nargs, lua.MultRet, nil) + if err != nil { + // Unwind the stack up to and including our sentinel value, effectively discarding any other returned parameters. + for { + v := l.Get(-1) + l.Pop(1) + if v.Type() == LTSentinel { + break + } + } + + if apiError, ok := err.(*lua.ApiError); ok && apiError.Object.Type() == lua.LTTable { + t := apiError.Object.(*lua.LTable) + switch t.Len() { + case 0: + return nil, err, codes.Internal + case 1: + apiError.Object = t.RawGetInt(1) + return nil, err, codes.Internal + default: + // Ignore everything beyond the first 2 params, if there are more. + apiError.Object = t.RawGetInt(1) + code := codes.Internal + if c := t.RawGetInt(2); c.Type() == lua.LTNumber { + code = codes.Code(c.(lua.LNumber)) + } + return nil, err, code + } + } + + return nil, err, codes.Internal + } + + retValue := l.Get(-1) + l.Pop(1) + if retValue.Type() == LTSentinel { + return nil, nil, 0 + } + + // Unwind the stack up to and including our sentinel value, effectively discarding any other returned parameters. + for { + v := l.Get(-1) + l.Pop(1) + if v.Type() == LTSentinel { + break + } + } + + return retValue, nil, 0 +} + +func (r *RuntimeLua) Stop() { + // Not necessarily required as it only does OS temp files cleanup, which we don't expose in the runtime. + r.vm.Close() +} + +func newRuntimeLuaVM(logger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, leaderboardCache LeaderboardCache, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter, stdLibs map[string]lua.LGFunction, moduleCache *RuntimeLuaModuleCache, once *sync.Once, localCache *RuntimeLuaLocalCache, matchCreateFn RuntimeMatchCreateFunction, announceCallback func(RuntimeExecutionMode, string)) (*RuntimeLua, error) { + // Initialize a one-off runtime to ensure startup code runs and modules are valid. + vm := lua.NewState(lua.Options{ + CallStackSize: config.GetRuntime().CallStackSize, + RegistrySize: config.GetRuntime().RegistrySize, + SkipOpenLibs: true, + IncludeGoStackTrace: true, + }) + for name, lib := range stdLibs { + vm.Push(vm.NewFunction(lib)) + vm.Push(lua.LString(name)) + vm.Call(1, 0) + } + nakamaModule := NewRuntimeLuaNakamaModule(logger, db, config, socialClient, leaderboardCache, vm, sessionRegistry, matchRegistry, tracker, router, once, localCache, matchCreateFn, announceCallback) + vm.PreloadModule("nakama", nakamaModule.Loader) + r := &RuntimeLua{ + logger: logger, + vm: vm, + luaEnv: RuntimeLuaConvertMapString(vm, config.GetRuntime().Environment), + } + + return r, r.loadModules(moduleCache) +} diff --git a/server/runtime_bit32.go b/server/runtime_lua_bit32.go similarity index 100% rename from server/runtime_bit32.go rename to server/runtime_lua_bit32.go diff --git a/server/runtime_lua_context.go b/server/runtime_lua_context.go index 7d7fbe91a544653c86a1c72057660af5a45fa9c3..124ed9868628bdc49087ae2a673605122b6460d6 100644 --- a/server/runtime_lua_context.go +++ b/server/runtime_lua_context.go @@ -20,54 +20,23 @@ import ( "github.com/yuin/gopher-lua" ) -type ExecutionMode int - const ( - ExecutionModeRunOnce ExecutionMode = iota - ExecutionModeRPC - ExecutionModeBefore - ExecutionModeAfter - ExecutionModeMatch - ExecutionModeMatchmaker + __RUNTIME_LUA_CTX_ENV = "env" + __RUNTIME_LUA_CTX_MODE = "execution_mode" + __RUNTIME_LUA_CTX_QUERY_PARAMS = "query_params" + __RUNTIME_LUA_CTX_USER_ID = "user_id" + __RUNTIME_LUA_CTX_USERNAME = "username" + __RUNTIME_LUA_CTX_USER_SESSION_EXP = "user_session_exp" + __RUNTIME_LUA_CTX_SESSION_ID = "session_id" + __RUNTIME_LUA_CTX_CLIENT_IP = "client_ip" + __RUNTIME_LUA_CTX_CLIENT_PORT = "client_port" + __RUNTIME_LUA_CTX_MATCH_ID = "match_id" + __RUNTIME_LUA_CTX_MATCH_NODE = "match_node" + __RUNTIME_LUA_CTX_MATCH_LABEL = "match_label" + __RUNTIME_LUA_CTX_MATCH_TICK_RATE = "match_tick_rate" ) -func (e ExecutionMode) String() string { - switch e { - case ExecutionModeRunOnce: - return "run_once" - case ExecutionModeRPC: - return "rpc" - case ExecutionModeBefore: - return "before" - case ExecutionModeAfter: - return "after" - case ExecutionModeMatch: - return "match" - case ExecutionModeMatchmaker: - return "matchmaker" - } - - return "" -} - -const ( - __CTX_ENV = "env" - __CTX_MODE = "execution_mode" - __CTX_QUERY_PARAMS = "query_params" - __CTX_USER_ID = "user_id" - __CTX_USERNAME = "username" - __CTX_USER_SESSION_EXP = "user_session_exp" - __CTX_SESSION_ID = "session_id" - __CTX_CLIENT_IP = "client_ip" - __CTX_CLIENT_PORT = "client_port" - __CTX_MATCH_ID = "match_id" - __CTX_MATCH_NODE = "match_node" - __CTX_MATCH_LABEL = "match_label" - __CTX_MATCH_TICK_RATE = "match_tick_rate" -) - -func NewLuaContext(l *lua.LState, env *lua.LTable, mode ExecutionMode, queryParams map[string][]string, sessionExpiry int64, - userID, username, sessionID, clientIP, clientPort string) *lua.LTable { +func NewRuntimeLuaContext(l *lua.LState, env *lua.LTable, mode RuntimeExecutionMode, queryParams map[string][]string, sessionExpiry int64, userID, username, sessionID, clientIP, clientPort string) *lua.LTable { size := 3 if userID != "" { size += 3 @@ -84,50 +53,59 @@ func NewLuaContext(l *lua.LState, env *lua.LTable, mode ExecutionMode, queryPara } lt := l.CreateTable(0, size) - lt.RawSetString(__CTX_ENV, env) - lt.RawSetString(__CTX_MODE, lua.LString(mode.String())) + lt.RawSetString(__RUNTIME_LUA_CTX_ENV, env) + lt.RawSetString(__RUNTIME_LUA_CTX_MODE, lua.LString(mode.String())) if queryParams == nil { - lt.RawSetString(__CTX_QUERY_PARAMS, l.CreateTable(0, 0)) + lt.RawSetString(__RUNTIME_LUA_CTX_QUERY_PARAMS, l.CreateTable(0, 0)) } else { - lt.RawSetString(__CTX_QUERY_PARAMS, ConvertValue(l, queryParams)) + lt.RawSetString(__RUNTIME_LUA_CTX_QUERY_PARAMS, RuntimeLuaConvertValue(l, queryParams)) } if userID != "" { - lt.RawSetString(__CTX_USER_ID, lua.LString(userID)) - lt.RawSetString(__CTX_USERNAME, lua.LString(username)) - lt.RawSetString(__CTX_USER_SESSION_EXP, lua.LNumber(sessionExpiry)) + lt.RawSetString(__RUNTIME_LUA_CTX_USER_ID, lua.LString(userID)) + lt.RawSetString(__RUNTIME_LUA_CTX_USERNAME, lua.LString(username)) + lt.RawSetString(__RUNTIME_LUA_CTX_USER_SESSION_EXP, lua.LNumber(sessionExpiry)) if sessionID != "" { - lt.RawSetString(__CTX_SESSION_ID, lua.LString(sessionID)) + lt.RawSetString(__RUNTIME_LUA_CTX_SESSION_ID, lua.LString(sessionID)) } } if clientIP != "" { - lt.RawSetString(__CTX_CLIENT_IP, lua.LString(clientIP)) + lt.RawSetString(__RUNTIME_LUA_CTX_CLIENT_IP, lua.LString(clientIP)) } - if clientPort != "" { - lt.RawSetString(__CTX_CLIENT_PORT, lua.LString(clientPort)) + lt.RawSetString(__RUNTIME_LUA_CTX_CLIENT_PORT, lua.LString(clientPort)) + } + + return lt +} + +func RuntimeLuaConvertMapString(l *lua.LState, data map[string]string) *lua.LTable { + lt := l.CreateTable(0, len(data)) + + for k, v := range data { + lt.RawSetString(k, RuntimeLuaConvertValue(l, v)) } return lt } -func ConvertMap(l *lua.LState, data map[string]interface{}) *lua.LTable { +func RuntimeLuaConvertMap(l *lua.LState, data map[string]interface{}) *lua.LTable { lt := l.CreateTable(0, len(data)) for k, v := range data { - lt.RawSetString(k, ConvertValue(l, v)) + lt.RawSetString(k, RuntimeLuaConvertValue(l, v)) } return lt } -func ConvertLuaTable(lv *lua.LTable) map[string]interface{} { - returnData, _ := ConvertLuaValue(lv).(map[string]interface{}) +func RuntimeLuaConvertLuaTable(lv *lua.LTable) map[string]interface{} { + returnData, _ := RuntimeLuaConvertLuaValue(lv).(map[string]interface{}) return returnData } -func ConvertValue(l *lua.LState, val interface{}) lua.LValue { +func RuntimeLuaConvertValue(l *lua.LState, val interface{}) lua.LValue { if val == nil { return lua.LNil } @@ -159,11 +137,13 @@ func ConvertValue(l *lua.LState, val interface{}) lua.LValue { case map[string][]string: lt := l.CreateTable(0, len(v)) for k, v := range v { - lt.RawSetString(k, ConvertValue(l, v)) + lt.RawSetString(k, RuntimeLuaConvertValue(l, v)) } return lt + case map[string]string: + return RuntimeLuaConvertMapString(l, v) case map[string]interface{}: - return ConvertMap(l, v) + return RuntimeLuaConvertMap(l, v) case []string: lt := l.CreateTable(len(val.([]string)), 0) for k, v := range v { @@ -173,7 +153,7 @@ func ConvertValue(l *lua.LState, val interface{}) lua.LValue { case []interface{}: lt := l.CreateTable(len(val.([]interface{})), 0) for k, v := range v { - lt.RawSetInt(k+1, ConvertValue(l, v)) + lt.RawSetInt(k+1, RuntimeLuaConvertValue(l, v)) } return lt default: @@ -181,7 +161,7 @@ func ConvertValue(l *lua.LState, val interface{}) lua.LValue { } } -func ConvertLuaValue(lv lua.LValue) interface{} { +func RuntimeLuaConvertLuaValue(lv lua.LValue) interface{} { // Taken from: https://github.com/yuin/gluamapper/blob/master/gluamapper.go#L79 switch v := lv.(type) { case *lua.LNilType: @@ -198,15 +178,15 @@ func ConvertLuaValue(lv lua.LValue) interface{} { // Table. ret := make(map[string]interface{}) v.ForEach(func(key, value lua.LValue) { - keystr := fmt.Sprint(ConvertLuaValue(key)) - ret[keystr] = ConvertLuaValue(value) + keyStr := fmt.Sprint(RuntimeLuaConvertLuaValue(key)) + ret[keyStr] = RuntimeLuaConvertLuaValue(value) }) return ret } else { // Array. ret := make([]interface{}, 0, maxn) for i := 1; i <= maxn; i++ { - ret = append(ret, ConvertLuaValue(v.RawGetInt(i))) + ret = append(ret, RuntimeLuaConvertLuaValue(v.RawGetInt(i))) } return ret } diff --git a/server/runtime_loadlib.go b/server/runtime_lua_loadlib.go similarity index 97% rename from server/runtime_loadlib.go rename to server/runtime_lua_loadlib.go index aea9437f7b25d460d003ba8ad45b78f9b1a9145b..c8c47b02313e0a8d67322595d14abd3bd2e7ee9e 100644 --- a/server/runtime_loadlib.go +++ b/server/runtime_lua_loadlib.go @@ -49,7 +49,7 @@ func loGetPath(env string, defpath string) string { return path } -func OpenPackage(moduleCache *ModuleCache) func(L *lua.LState) int { +func OpenPackage(moduleCache *RuntimeLuaModuleCache) func(L *lua.LState) int { return func(L *lua.LState) int { loLoaderCache := func(L *lua.LState) int { name := L.CheckString(1) diff --git a/server/runtime_lua_localcache.go b/server/runtime_lua_localcache.go new file mode 100644 index 0000000000000000000000000000000000000000..447714d8dfb7c9dec0c1628d988dd912b133d89a --- /dev/null +++ b/server/runtime_lua_localcache.go @@ -0,0 +1,47 @@ +// Copyright 2018 The Nakama Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import "sync" + +type RuntimeLuaLocalCache struct { + sync.RWMutex + data map[string]string +} + +func NewRuntimeLuaLocalCache() *RuntimeLuaLocalCache { + return &RuntimeLuaLocalCache{ + data: make(map[string]string), + } +} + +func (lc *RuntimeLuaLocalCache) Get(key string) (string, bool) { + lc.RLock() + value, found := lc.data[key] + lc.RUnlock() + return value, found +} + +func (lc *RuntimeLuaLocalCache) Put(key, value string) { + lc.Lock() + lc.data[key] = value + lc.Unlock() +} + +func (lc *RuntimeLuaLocalCache) Delete(key string) { + lc.Lock() + delete(lc.data, key) + lc.Unlock() +} diff --git a/server/runtime_lua_match_core.go b/server/runtime_lua_match_core.go new file mode 100644 index 0000000000000000000000000000000000000000..0844073d1944d763c5318d0f52e9fca2214362a9 --- /dev/null +++ b/server/runtime_lua_match_core.go @@ -0,0 +1,660 @@ +// Copyright 2018 The Nakama Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import ( + "database/sql" + "errors" + "fmt" + "github.com/gofrs/uuid" + "github.com/heroiclabs/nakama/rtapi" + "github.com/heroiclabs/nakama/social" + "github.com/yuin/gopher-lua" + "go.uber.org/zap" + "sync" +) + +type RuntimeLuaMatchCore struct { + logger *zap.Logger + matchRegistry MatchRegistry + tracker Tracker + router MessageRouter + + labelUpdateFn func(string) + + id uuid.UUID + node string + idStr string + stream PresenceStream + + vm *lua.LState + initFn lua.LValue + joinAttemptFn lua.LValue + joinFn lua.LValue + leaveFn lua.LValue + loopFn lua.LValue + ctx *lua.LTable + dispatcher *lua.LTable +} + +func NewRuntimeLuaMatchCore(logger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, leaderboardCache LeaderboardCache, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter, stdLibs map[string]lua.LGFunction, once *sync.Once, localCache *RuntimeLuaLocalCache, goMatchCreateFn RuntimeMatchCreateFunction, id uuid.UUID, node string, name string, labelUpdateFn func(string)) (RuntimeMatchCore, error) { + // Set up the Lua VM that will handle this match. + vm := lua.NewState(lua.Options{ + CallStackSize: config.GetRuntime().CallStackSize, + RegistrySize: config.GetRuntime().RegistrySize, + SkipOpenLibs: true, + IncludeGoStackTrace: true, + }) + for name, lib := range stdLibs { + vm.Push(vm.NewFunction(lib)) + vm.Push(lua.LString(name)) + vm.Call(1, 0) + } + + allMatchCreateFn := func(logger *zap.Logger, id uuid.UUID, node string, name string, labelUpdateFn func(string)) (RuntimeMatchCore, error) { + core, err := goMatchCreateFn(logger, id, node, name, labelUpdateFn) + if err != nil { + return nil, err + } + if core != nil { + return core, nil + } + return NewRuntimeLuaMatchCore(logger, db, config, socialClient, leaderboardCache, sessionRegistry, matchRegistry, tracker, router, stdLibs, once, localCache, goMatchCreateFn, id, node, name, labelUpdateFn) + } + + nakamaModule := NewRuntimeLuaNakamaModule(logger, db, config, socialClient, leaderboardCache, vm, sessionRegistry, matchRegistry, tracker, router, once, localCache, allMatchCreateFn, nil) + vm.PreloadModule("nakama", nakamaModule.Loader) + + // Create the context to be used throughout this match. + ctx := vm.CreateTable(0, 6) + ctx.RawSetString(__RUNTIME_LUA_CTX_ENV, RuntimeLuaConvertMapString(vm, config.GetRuntime().Environment)) + ctx.RawSetString(__RUNTIME_LUA_CTX_MODE, lua.LString(RuntimeExecutionModeMatch.String())) + ctx.RawSetString(__RUNTIME_LUA_CTX_MATCH_ID, lua.LString(fmt.Sprintf("%v.%v", id.String(), node))) + ctx.RawSetString(__RUNTIME_LUA_CTX_MATCH_NODE, lua.LString(node)) + + // Require the match module to load it (and its dependencies) and get its returned value. + req := vm.GetGlobal("require").(*lua.LFunction) + err := vm.GPCall(req.GFunction, lua.LString(name)) + if err != nil { + return nil, fmt.Errorf("error loading match module: %v", err.Error()) + } + + // Extract the expected function references. + var tab *lua.LTable + if t := vm.Get(-1); t.Type() != lua.LTTable { + return nil, errors.New("match module must return a table containing the match callback functions") + } else { + tab = t.(*lua.LTable) + } + initFn := tab.RawGet(lua.LString("match_init")) + if initFn.Type() != lua.LTFunction { + return nil, errors.New("match_init not found or not a function") + } + joinAttemptFn := tab.RawGet(lua.LString("match_join_attempt")) + if joinAttemptFn.Type() != lua.LTFunction { + return nil, errors.New("match_join_attempt not found or not a function") + } + joinFn := tab.RawGet(lua.LString("match_join")) + if joinFn == nil || joinFn.Type() != lua.LTFunction { + joinFn = nil + } + leaveFn := tab.RawGet(lua.LString("match_leave")) + if leaveFn.Type() != lua.LTFunction { + return nil, errors.New("match_leave not found or not a function") + } + loopFn := tab.RawGet(lua.LString("match_loop")) + if loopFn.Type() != lua.LTFunction { + return nil, errors.New("match_loop not found or not a function") + } + + core := &RuntimeLuaMatchCore{ + logger: logger, + matchRegistry: matchRegistry, + tracker: tracker, + router: router, + + labelUpdateFn: labelUpdateFn, + + id: id, + node: node, + idStr: fmt.Sprintf("%v.%v", id.String(), node), + stream: PresenceStream{ + Mode: StreamModeMatchAuthoritative, + Subject: id, + Label: node, + }, + + vm: vm, + initFn: initFn, + joinAttemptFn: joinAttemptFn, + joinFn: joinFn, + leaveFn: leaveFn, + loopFn: loopFn, + ctx: ctx, + // dispatcher set below. + } + + core.dispatcher = vm.SetFuncs(vm.CreateTable(0, 3), map[string]lua.LGFunction{ + "broadcast_message": core.broadcastMessage, + "match_kick": core.matchKick, + "match_label_update": core.matchLabelUpdate, + }) + + return core, nil +} + +func (r *RuntimeLuaMatchCore) MatchInit(params map[string]interface{}) (interface{}, int, string, error) { + // Run the match_init sequence. + r.vm.Push(LSentinel) + r.vm.Push(r.initFn) + r.vm.Push(r.ctx) + if params == nil { + r.vm.Push(lua.LNil) + } else { + r.vm.Push(RuntimeLuaConvertMap(r.vm, params)) + } + + err := r.vm.PCall(2, lua.MultRet, nil) + if err != nil { + return nil, 0, "", err + } + + // Extract desired label. + label := r.vm.Get(-1) + if label.Type() == LTSentinel { + return nil, 0, "", errors.New("match_init returned unexpected third value, must be a label string") + } else if label.Type() != lua.LTString { + return nil, 0, "", errors.New("match_init returned unexpected third value, must be a label string") + } + r.vm.Pop(1) + + labelStr := label.String() + if len(labelStr) > 256 { + return nil, 0, "", errors.New("match_init returned invalid label, must be 256 bytes or less") + } + + // Extract desired tick rate. + rate := r.vm.Get(-1) + if rate.Type() == LTSentinel { + return nil, 0, "", errors.New("match_init returned unexpected second value, must be a tick rate number") + } else if rate.Type() != lua.LTNumber { + return nil, 0, "", errors.New("match_init returned unexpected second value, must be a tick rate number") + } + r.vm.Pop(1) + + rateInt := int(rate.(lua.LNumber)) + if rateInt > 30 || rateInt < 1 { + return nil, 0, "", errors.New("match_init returned invalid tick rate, must be between 1 and 30") + } + + // Extract initial state. + state := r.vm.Get(-1) + if state.Type() == LTSentinel { + return nil, 0, "", errors.New("match_init returned unexpected first value, must be a state") + } + r.vm.Pop(1) + + // Drop the sentinel value from the stack. + if sentinel := r.vm.Get(-1); sentinel.Type() != LTSentinel { + return nil, 0, "", errors.New("match_init returned too many arguments, must be: state, tick rate number, label string") + } + r.vm.Pop(1) + + // Add context values only available after match_init completes. + r.ctx.RawSetString(__RUNTIME_LUA_CTX_MATCH_LABEL, label) + r.ctx.RawSetString(__RUNTIME_LUA_CTX_MATCH_TICK_RATE, rate) + + return state, rateInt, labelStr, nil +} + +func (r *RuntimeLuaMatchCore) MatchJoinAttempt(tick int64, state interface{}, userID, sessionID uuid.UUID, username, node string) (interface{}, bool, string, error) { + presence := r.vm.CreateTable(0, 4) + presence.RawSetString("user_id", lua.LString(userID.String())) + presence.RawSetString("session_id", lua.LString(sessionID.String())) + presence.RawSetString("username", lua.LString(username)) + presence.RawSetString("node", lua.LString(node)) + + // Execute the match_join_attempt call. + r.vm.Push(LSentinel) + r.vm.Push(r.joinAttemptFn) + r.vm.Push(r.ctx) + r.vm.Push(r.dispatcher) + r.vm.Push(lua.LNumber(tick)) + r.vm.Push(state.(lua.LValue)) + r.vm.Push(presence) + + err := r.vm.PCall(5, lua.MultRet, nil) + if err != nil { + return nil, false, "", err + } + + allowFound := false + var allow bool + var reason string + + // Extract the join attempt response. + allowOrReason := r.vm.Get(-1) + if allowOrReason.Type() == LTSentinel { + return nil, false, "", errors.New("Match join attempt returned too few values, stopping match - expected: state, join result boolean, optional reject reason string") + } else if allowOrReason.Type() == lua.LTString { + // This was the optional reject reason string. + reason = allowOrReason.String() + } else if allowOrReason.Type() == lua.LTBool { + // This was the required join result boolean, expect no reason as it was skipped. + allowFound = true + allow = lua.LVAsBool(allowOrReason) + } else { + return nil, false, "", errors.New("Match join attempt returned non-boolean join result or non-string reject reason, stopping match") + } + r.vm.Pop(1) + + if !allowFound { + // The previous parameter was the optional reject reason string, now look for the required join result boolean. + allowRequired := r.vm.Get(-1) + if allowRequired.Type() == LTSentinel { + return nil, false, "", errors.New("Match join attempt returned incorrect or too few values, stopping match - expected: state, join result boolean, optional reject reason string") + } else if allowRequired.Type() != lua.LTBool { + return nil, false, "", errors.New("Match join attempt returned non-boolean join result, stopping match") + } + allow = lua.LVAsBool(allowRequired) + r.vm.Pop(1) + } + + // Extract the resulting state. + newState := r.vm.Get(-1) + if newState.Type() == lua.LTNil || newState.Type() == LTSentinel { + return nil, false, "", nil + } + r.vm.Pop(1) + // Check for and remove the sentinel value, will fail if there are any extra return values. + if sentinel := r.vm.Get(-1); sentinel.Type() != LTSentinel { + return nil, false, "", errors.New("Match join attempt returned too many values, stopping match") + } + r.vm.Pop(1) + + return newState, allow, reason, nil +} + +func (r *RuntimeLuaMatchCore) MatchJoin(tick int64, state interface{}, joins []*MatchPresence) (interface{}, error) { + if r.joinFn == nil { + return state, nil + } + + presences := r.vm.CreateTable(len(joins), 0) + for i, p := range joins { + presence := r.vm.CreateTable(0, 4) + presence.RawSetString("user_id", lua.LString(p.UserID.String())) + presence.RawSetString("session_id", lua.LString(p.SessionID.String())) + presence.RawSetString("username", lua.LString(p.Username)) + presence.RawSetString("node", lua.LString(p.Node)) + + presences.RawSetInt(i+1, presence) + } + + // Execute the match_leave call. + r.vm.Push(LSentinel) + r.vm.Push(r.joinFn) + r.vm.Push(r.ctx) + r.vm.Push(r.dispatcher) + r.vm.Push(lua.LNumber(tick)) + r.vm.Push(state.(lua.LValue)) + r.vm.Push(presences) + + err := r.vm.PCall(5, lua.MultRet, nil) + if err != nil { + return nil, err + } + + // Extract the resulting state. + newState := r.vm.Get(-1) + if newState.Type() == lua.LTNil || newState.Type() == LTSentinel { + return nil, nil + } + r.vm.Pop(1) + // Check for and remove the sentinel value, will fail if there are any extra return values. + if sentinel := r.vm.Get(-1); sentinel.Type() != LTSentinel { + return nil, errors.New("Match join returned too many values, stopping match") + } + r.vm.Pop(1) + + return newState, nil +} + +func (r *RuntimeLuaMatchCore) MatchLeave(tick int64, state interface{}, leaves []*MatchPresence) (interface{}, error) { + presences := r.vm.CreateTable(len(leaves), 0) + for i, p := range leaves { + presence := r.vm.CreateTable(0, 4) + presence.RawSetString("user_id", lua.LString(p.UserID.String())) + presence.RawSetString("session_id", lua.LString(p.SessionID.String())) + presence.RawSetString("username", lua.LString(p.Username)) + presence.RawSetString("node", lua.LString(p.Node)) + + presences.RawSetInt(i+1, presence) + } + + // Execute the match_leave call. + r.vm.Push(LSentinel) + r.vm.Push(r.leaveFn) + r.vm.Push(r.ctx) + r.vm.Push(r.dispatcher) + r.vm.Push(lua.LNumber(tick)) + r.vm.Push(state.(lua.LValue)) + r.vm.Push(presences) + + err := r.vm.PCall(5, lua.MultRet, nil) + if err != nil { + return nil, err + } + + // Extract the resulting state. + newState := r.vm.Get(-1) + if newState.Type() == lua.LTNil || newState.Type() == LTSentinel { + return nil, nil + } + r.vm.Pop(1) + // Check for and remove the sentinel value, will fail if there are any extra return values. + if sentinel := r.vm.Get(-1); sentinel.Type() != LTSentinel { + return nil, errors.New("Match leave returned too many values, stopping match") + } + r.vm.Pop(1) + + return newState, nil +} + +func (r *RuntimeLuaMatchCore) MatchLoop(tick int64, state interface{}, inputCh chan *MatchDataMessage) (interface{}, error) { + // Drain the input queue into a Lua table. + size := len(inputCh) + input := r.vm.CreateTable(size, 0) + for i := 1; i <= size; i++ { + msg := <-inputCh + + presence := r.vm.CreateTable(0, 4) + presence.RawSetString("user_id", lua.LString(msg.UserID.String())) + presence.RawSetString("session_id", lua.LString(msg.SessionID.String())) + presence.RawSetString("username", lua.LString(msg.Username)) + presence.RawSetString("node", lua.LString(msg.Node)) + + in := r.vm.CreateTable(0, 4) + in.RawSetString("sender", presence) + in.RawSetString("op_code", lua.LNumber(msg.OpCode)) + if msg.Data != nil { + in.RawSetString("data", lua.LString(msg.Data)) + } else { + in.RawSetString("data", lua.LNil) + } + in.RawSetString("receive_time_ms", lua.LNumber(msg.ReceiveTime)) + + input.RawSetInt(i, in) + } + + // Execute the match_loop call. + r.vm.Push(LSentinel) + r.vm.Push(r.loopFn) + r.vm.Push(r.ctx) + r.vm.Push(r.dispatcher) + r.vm.Push(lua.LNumber(tick)) + r.vm.Push(state.(lua.LValue)) + r.vm.Push(input) + + err := r.vm.PCall(5, lua.MultRet, nil) + if err != nil { + return nil, err + } + + // Extract the resulting state. + newState := r.vm.Get(-1) + if newState.Type() == lua.LTNil || newState.Type() == LTSentinel { + return nil, nil + } + r.vm.Pop(1) + // Check for and remove the sentinel value, will fail if there are any extra return values. + if sentinel := r.vm.Get(-1); sentinel.Type() != LTSentinel { + return nil, errors.New("Match loop returned too many values, stopping match") + } + r.vm.Pop(1) + + return newState, nil +} + +func (r *RuntimeLuaMatchCore) broadcastMessage(l *lua.LState) int { + opCode := l.CheckInt64(1) + + var dataBytes []byte + if data := l.Get(2); data.Type() != lua.LTNil { + if data.Type() != lua.LTString { + l.ArgError(2, "expects data to be a string or nil") + return 0 + } + dataBytes = []byte(data.(lua.LString)) + } + + filter := l.OptTable(3, nil) + var presenceIDs []*PresenceID + if filter != nil { + fl := filter.Len() + if fl == 0 { + return 0 + } + presenceIDs = make([]*PresenceID, 0, fl) + conversionError := false + filter.ForEach(func(_, p lua.LValue) { + pt, ok := p.(*lua.LTable) + if !ok { + conversionError = true + l.ArgError(1, "expects a valid set of presences") + return + } + + presenceID := &PresenceID{} + pt.ForEach(func(k, v lua.LValue) { + switch k.String() { + case "session_id": + sid, err := uuid.FromString(v.String()) + if err != nil { + conversionError = true + l.ArgError(1, "expects each presence to have a valid session_id") + return + } + presenceID.SessionID = sid + case "node": + if v.Type() != lua.LTString { + conversionError = true + l.ArgError(1, "expects node to be string") + return + } + presenceID.Node = v.String() + } + }) + if presenceID.SessionID == uuid.Nil || presenceID.Node == "" { + conversionError = true + l.ArgError(1, "expects each presence to have a valid session_id and node") + return + } + if conversionError { + return + } + presenceIDs = append(presenceIDs, presenceID) + }) + if conversionError { + return 0 + } + } + + if presenceIDs != nil && len(presenceIDs) == 0 { + // Filter is empty, there are no requested message targets. + return 0 + } + + sender := l.OptTable(4, nil) + var presence *rtapi.UserPresence + if sender != nil { + presence = &rtapi.UserPresence{} + conversionError := false + sender.ForEach(func(k, v lua.LValue) { + switch k.String() { + case "user_id": + s := v.String() + _, err := uuid.FromString(s) + if err != nil { + conversionError = true + l.ArgError(4, "expects presence to have a valid user_id") + return + } + presence.UserId = s + case "session_id": + s := v.String() + _, err := uuid.FromString(s) + if err != nil { + conversionError = true + l.ArgError(4, "expects presence to have a valid session_id") + return + } + presence.SessionId = s + case "username": + if v.Type() != lua.LTString { + conversionError = true + l.ArgError(4, "expects username to be string") + return + } + presence.Username = v.String() + } + }) + if presence.UserId == "" || presence.SessionId == "" || presence.Username == "" { + l.ArgError(4, "expects presence to have a valid user_id, session_id, and username") + return 0 + } + if conversionError { + return 0 + } + } + + if presenceIDs != nil { + // Ensure specific presences actually exist to prevent sending bogus messages to arbitrary users. + actualPresenceIDs := r.tracker.ListPresenceIDByStream(r.stream) + for i := 0; i < len(presenceIDs); i++ { + found := false + presenceID := presenceIDs[i] + for j := 0; j < len(actualPresenceIDs); j++ { + if actual := actualPresenceIDs[j]; presenceID.SessionID == actual.SessionID && presenceID.Node == actual.Node { + // If it matches, drop it. + actualPresenceIDs[j] = actualPresenceIDs[len(actualPresenceIDs)-1] + actualPresenceIDs = actualPresenceIDs[:len(actualPresenceIDs)-1] + found = true + break + } + } + if !found { + // If this presence wasn't in the filters, it's not needed. + presenceIDs[i] = presenceIDs[len(presenceIDs)-1] + presenceIDs = presenceIDs[:len(presenceIDs)-1] + i-- + } + } + if len(presenceIDs) == 0 { + // None of the target presenceIDs existed in the list of match members. + return 0 + } + } + + msg := &rtapi.Envelope{Message: &rtapi.Envelope_MatchData{MatchData: &rtapi.MatchData{ + MatchId: r.idStr, + Presence: presence, + OpCode: opCode, + Data: dataBytes, + }}} + + if presenceIDs == nil { + r.router.SendToStream(r.logger, r.stream, msg) + } else { + r.router.SendToPresenceIDs(r.logger, presenceIDs, true, StreamModeMatchAuthoritative, msg) + } + + return 0 +} + +func (r *RuntimeLuaMatchCore) matchKick(l *lua.LState) int { + input := l.OptTable(1, nil) + if input == nil { + return 0 + } + size := input.Len() + if size == 0 { + return 0 + } + + presences := make([]*MatchPresence, 0, size) + conversionError := false + input.ForEach(func(_, p lua.LValue) { + pt, ok := p.(*lua.LTable) + if !ok { + conversionError = true + l.ArgError(1, "expects a valid set of presences") + return + } + + presence := &MatchPresence{} + pt.ForEach(func(k, v lua.LValue) { + switch k.String() { + case "user_id": + uid, err := uuid.FromString(v.String()) + if err != nil { + conversionError = true + l.ArgError(1, "expects each presence to have a valid user_id") + return + } + presence.UserID = uid + case "session_id": + sid, err := uuid.FromString(v.String()) + if err != nil { + conversionError = true + l.ArgError(1, "expects each presence to have a valid session_id") + return + } + presence.SessionID = sid + case "node": + if v.Type() != lua.LTString { + conversionError = true + l.ArgError(1, "expects node to be string") + return + } + presence.Node = v.String() + } + }) + if presence.UserID == uuid.Nil || presence.SessionID == uuid.Nil || presence.Node == "" { + conversionError = true + l.ArgError(1, "expects each presence to have a valid user_id, session_id, and node") + return + } + if conversionError { + return + } + presences = append(presences, presence) + }) + if conversionError { + return 0 + } + + r.matchRegistry.Kick(r.stream, presences) + return 0 +} + +func (r *RuntimeLuaMatchCore) matchLabelUpdate(l *lua.LState) int { + input := l.OptString(1, "") + + r.labelUpdateFn(input) + // This must be executed from inside a match call so safe to update here. + r.ctx.RawSetString(__RUNTIME_LUA_CTX_MATCH_LABEL, lua.LString(input)) + return 0 +} diff --git a/server/runtime_nakama_module.go b/server/runtime_lua_nakama.go similarity index 89% rename from server/runtime_nakama_module.go rename to server/runtime_lua_nakama.go index a43faaae3e48229ca42224bc2ceabc7bf61e69c2..e697778ef07475737780f7ffff966bc945079a02 100644 --- a/server/runtime_nakama_module.go +++ b/server/runtime_lua_nakama.go @@ -16,32 +16,17 @@ package server import ( "context" - "fmt" - "io" - "net/http" - "time" - - "strings" - - "database/sql" - - "encoding/json" - - "encoding/base64" - - "encoding/hex" - "io/ioutil" - "crypto/aes" "crypto/cipher" - "crypto/rand" - "sync" - "crypto/hmac" - "crypto/sha256" - "crypto/md5" - + "crypto/rand" + "crypto/sha256" + "database/sql" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" "github.com/gofrs/uuid" "github.com/golang/protobuf/ptypes/timestamp" "github.com/golang/protobuf/ptypes/wrappers" @@ -50,22 +35,27 @@ import ( "github.com/heroiclabs/nakama/rtapi" "github.com/heroiclabs/nakama/social" "github.com/yuin/gopher-lua" + "go.uber.org/atomic" "go.uber.org/zap" "golang.org/x/crypto/bcrypt" + "io" + "io/ioutil" + "net/http" + "strings" + "sync" + "time" ) -const CALLBACKS = "runtime_callbacks" -const API_PREFIX = "/nakama.api.Nakama/" -const RTAPI_PREFIX = "*rtapi.Envelope_" +const RUNTIME_LUA_CALLBACKS = "runtime_lua_callbacks" -type Callbacks struct { +type RuntimeLuaCallbacks struct { RPC map[string]*lua.LFunction Before map[string]*lua.LFunction After map[string]*lua.LFunction Matchmaker *lua.LFunction } -type NakamaModule struct { +type RuntimeLuaNakamaModule struct { logger *zap.Logger db *sql.DB config Config @@ -76,17 +66,21 @@ type NakamaModule struct { tracker Tracker router MessageRouter once *sync.Once - announceCallback func(ExecutionMode, string) + localCache *RuntimeLuaLocalCache + announceCallback func(RuntimeExecutionMode, string) client *http.Client + + node string + matchCreateFn RuntimeMatchCreateFunction } -func NewNakamaModule(logger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, leaderboardCache LeaderboardCache, l *lua.LState, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter, once *sync.Once, announceCallback func(ExecutionMode, string)) *NakamaModule { - l.SetContext(context.WithValue(context.Background(), CALLBACKS, &Callbacks{ +func NewRuntimeLuaNakamaModule(logger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, leaderboardCache LeaderboardCache, l *lua.LState, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter, once *sync.Once, localCache *RuntimeLuaLocalCache, matchCreateFn RuntimeMatchCreateFunction, announceCallback func(RuntimeExecutionMode, string)) *RuntimeLuaNakamaModule { + l.SetContext(context.WithValue(context.Background(), RUNTIME_LUA_CALLBACKS, &RuntimeLuaCallbacks{ RPC: make(map[string]*lua.LFunction), Before: make(map[string]*lua.LFunction), After: make(map[string]*lua.LFunction), })) - return &NakamaModule{ + return &RuntimeLuaNakamaModule{ logger: logger, db: db, config: config, @@ -97,14 +91,18 @@ func NewNakamaModule(logger *zap.Logger, db *sql.DB, config Config, socialClient tracker: tracker, router: router, once: once, + localCache: localCache, announceCallback: announceCallback, client: &http.Client{ Timeout: 5 * time.Second, }, + + node: config.GetName(), + matchCreateFn: matchCreateFn, } } -func (n *NakamaModule) Loader(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) Loader(l *lua.LState) int { functions := map[string]lua.LGFunction{ "register_rpc": n.registerRPC, "register_req_before": n.registerReqBefore, @@ -113,6 +111,9 @@ func (n *NakamaModule) Loader(l *lua.LState) int { "register_rt_after": n.registerRTAfter, "register_matchmaker_matched": n.registerMatchmakerMatched, "run_once": n.runOnce, + "localcache_get": n.localcacheGet, + "localcache_put": n.localcachePut, + "localcache_delete": n.localcacheDelete, "time": n.time, "cron_next": n.cronNext, "sql_exec": n.sqlExec, @@ -190,7 +191,7 @@ func (n *NakamaModule) Loader(l *lua.LState) int { return 1 } -func (n *NakamaModule) registerRPC(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) registerRPC(l *lua.LState) int { fn := l.CheckFunction(1) id := l.CheckString(2) @@ -201,19 +202,19 @@ func (n *NakamaModule) registerRPC(l *lua.LState) int { id = strings.ToLower(id) - rc := l.Context().Value(CALLBACKS).(*Callbacks) - if _, ok := rc.RPC[id]; ok { - //l.RaiseError("rpc id already registered") - return 0 - } + rc := l.Context().Value(RUNTIME_LUA_CALLBACKS).(*RuntimeLuaCallbacks) + //if _, ok := rc.RPC[id]; ok { + // l.RaiseError("rpc id already registered") + // return 0 + //} rc.RPC[id] = fn if n.announceCallback != nil { - n.announceCallback(ExecutionModeRPC, id) + n.announceCallback(RuntimeExecutionModeRPC, id) } return 0 } -func (n *NakamaModule) registerReqBefore(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) registerReqBefore(l *lua.LState) int { fn := l.CheckFunction(1) id := l.CheckString(2) @@ -224,19 +225,19 @@ func (n *NakamaModule) registerReqBefore(l *lua.LState) int { id = strings.ToLower(API_PREFIX + id) - rc := l.Context().Value(CALLBACKS).(*Callbacks) - if _, ok := rc.Before[id]; ok { - //l.RaiseError("before id already registered") - return 0 - } + rc := l.Context().Value(RUNTIME_LUA_CALLBACKS).(*RuntimeLuaCallbacks) + //if _, ok := rc.Before[id]; ok { + // l.RaiseError("before id already registered") + // return 0 + //} rc.Before[id] = fn if n.announceCallback != nil { - n.announceCallback(ExecutionModeBefore, id) + n.announceCallback(RuntimeExecutionModeBefore, id) } return 0 } -func (n *NakamaModule) registerReqAfter(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) registerReqAfter(l *lua.LState) int { fn := l.CheckFunction(1) id := l.CheckString(2) @@ -247,19 +248,19 @@ func (n *NakamaModule) registerReqAfter(l *lua.LState) int { id = strings.ToLower(API_PREFIX + id) - rc := l.Context().Value(CALLBACKS).(*Callbacks) - if _, ok := rc.After[id]; ok { - //l.RaiseError("after id already registered") - return 0 - } + rc := l.Context().Value(RUNTIME_LUA_CALLBACKS).(*RuntimeLuaCallbacks) + //if _, ok := rc.After[id]; ok { + // l.RaiseError("after id already registered") + // return 0 + //} rc.After[id] = fn if n.announceCallback != nil { - n.announceCallback(ExecutionModeAfter, id) + n.announceCallback(RuntimeExecutionModeAfter, id) } return 0 } -func (n *NakamaModule) registerRTBefore(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) registerRTBefore(l *lua.LState) int { fn := l.CheckFunction(1) id := l.CheckString(2) @@ -270,19 +271,19 @@ func (n *NakamaModule) registerRTBefore(l *lua.LState) int { id = strings.ToLower(RTAPI_PREFIX + id) - rc := l.Context().Value(CALLBACKS).(*Callbacks) - if _, ok := rc.Before[id]; ok { - //l.RaiseError("before id already registered") - return 0 - } + rc := l.Context().Value(RUNTIME_LUA_CALLBACKS).(*RuntimeLuaCallbacks) + //if _, ok := rc.Before[id]; ok { + // l.RaiseError("before id already registered") + // return 0 + //} rc.Before[id] = fn if n.announceCallback != nil { - n.announceCallback(ExecutionModeBefore, id) + n.announceCallback(RuntimeExecutionModeBefore, id) } return 0 } -func (n *NakamaModule) registerRTAfter(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) registerRTAfter(l *lua.LState) int { fn := l.CheckFunction(1) id := l.CheckString(2) @@ -293,34 +294,34 @@ func (n *NakamaModule) registerRTAfter(l *lua.LState) int { id = strings.ToLower(RTAPI_PREFIX + id) - rc := l.Context().Value(CALLBACKS).(*Callbacks) + rc := l.Context().Value(RUNTIME_LUA_CALLBACKS).(*RuntimeLuaCallbacks) if _, ok := rc.After[id]; ok { //l.RaiseError("before id already registered") return 0 } rc.After[id] = fn if n.announceCallback != nil { - n.announceCallback(ExecutionModeAfter, id) + n.announceCallback(RuntimeExecutionModeAfter, id) } return 0 } -func (n *NakamaModule) registerMatchmakerMatched(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) registerMatchmakerMatched(l *lua.LState) int { fn := l.CheckFunction(1) - rc := l.Context().Value(CALLBACKS).(*Callbacks) + rc := l.Context().Value(RUNTIME_LUA_CALLBACKS).(*RuntimeLuaCallbacks) if rc.Matchmaker != nil { //l.RaiseError("matchmaker matched already registered") return 0 } rc.Matchmaker = fn if n.announceCallback != nil { - n.announceCallback(ExecutionModeMatchmaker, "") + n.announceCallback(RuntimeExecutionModeMatchmaker, "") } return 0 } -func (n *NakamaModule) runOnce(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) runOnce(l *lua.LState) int { n.once.Do(func() { fn := l.CheckFunction(1) if fn == nil { @@ -328,7 +329,7 @@ func (n *NakamaModule) runOnce(l *lua.LState) int { return } - ctx := NewLuaContext(l, ConvertMap(l, n.config.GetRuntime().Environment), ExecutionModeRunOnce, nil, 0, "", "", "", "", "") + ctx := NewRuntimeLuaContext(l, RuntimeLuaConvertMapString(l, n.config.GetRuntime().Environment), RuntimeExecutionModeRunOnce, nil, 0, "", "", "", "", "") l.Push(LSentinel) l.Push(fn) @@ -351,7 +352,60 @@ func (n *NakamaModule) runOnce(l *lua.LState) int { return 0 } -func (n *NakamaModule) time(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) localcacheGet(l *lua.LState) int { + key := l.CheckString(1) + if key == "" { + l.ArgError(1, "expects key string") + return 0 + } + + defaultValue := l.Get(2) + if t := defaultValue.Type(); t != lua.LTNil && t != lua.LTString { + l.ArgError(2, "expects default value string or nil") + return 0 + } + + value, found := n.localCache.Get(key) + + if found { + l.Push(lua.LString(value)) + } else { + l.Push(defaultValue) + } + return 1 +} + +func (n *RuntimeLuaNakamaModule) localcachePut(l *lua.LState) int { + key := l.CheckString(1) + if key == "" { + l.ArgError(1, "expects key string") + return 0 + } + + value := l.CheckString(2) + if value == "" { + l.ArgError(2, "expects value string") + return 0 + } + + n.localCache.Put(key, value) + + return 0 +} + +func (n *RuntimeLuaNakamaModule) localcacheDelete(l *lua.LState) int { + key := l.CheckString(1) + if key == "" { + l.ArgError(1, "expects key string") + return 0 + } + + n.localCache.Delete(key) + + return 0 +} + +func (n *RuntimeLuaNakamaModule) time(l *lua.LState) int { if l.GetTop() == 0 { l.Push(lua.LNumber(time.Now().UTC().UnixNano() / int64(time.Millisecond))) } else { @@ -374,7 +428,7 @@ func (n *NakamaModule) time(l *lua.LState) int { return 1 } -func (n *NakamaModule) cronNext(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) cronNext(l *lua.LState) int { cron := l.CheckString(1) if cron == "" { l.ArgError(1, "expects cron string") @@ -398,7 +452,7 @@ func (n *NakamaModule) cronNext(l *lua.LState) int { return 1 } -func (n *NakamaModule) sqlExec(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) sqlExec(l *lua.LState) int { query := l.CheckString(1) if query == "" { l.ArgError(1, "expects query string") @@ -408,7 +462,7 @@ func (n *NakamaModule) sqlExec(l *lua.LState) int { var params []interface{} if paramsTable != nil && paramsTable.Len() != 0 { var ok bool - params, ok = ConvertLuaValue(paramsTable).([]interface{}) + params, ok = RuntimeLuaConvertLuaValue(paramsTable).([]interface{}) if !ok { l.ArgError(2, "expects a list of params as a table") return 0 @@ -430,7 +484,7 @@ func (n *NakamaModule) sqlExec(l *lua.LState) int { return 1 } -func (n *NakamaModule) sqlQuery(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) sqlQuery(l *lua.LState) int { query := l.CheckString(1) if query == "" { l.ArgError(1, "expects query string") @@ -440,7 +494,7 @@ func (n *NakamaModule) sqlQuery(l *lua.LState) int { var params []interface{} if paramsTable != nil && paramsTable.Len() != 0 { var ok bool - params, ok = ConvertLuaValue(paramsTable).([]interface{}) + params, ok = RuntimeLuaConvertLuaValue(paramsTable).([]interface{}) if !ok { l.ArgError(2, "expects a list of params as a table") return 0 @@ -482,7 +536,7 @@ func (n *NakamaModule) sqlQuery(l *lua.LState) int { for i, r := range resultRows { rowTable := l.CreateTable(0, resultColumnCount) for j, col := range resultColumns { - rowTable.RawSetString(col, ConvertValue(l, r[j])) + rowTable.RawSetString(col, RuntimeLuaConvertValue(l, r[j])) } rt.RawSetInt(i+1, rowTable) } @@ -490,12 +544,12 @@ func (n *NakamaModule) sqlQuery(l *lua.LState) int { return 1 } -func (n *NakamaModule) uuidV4(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) uuidV4(l *lua.LState) int { l.Push(lua.LString(uuid.Must(uuid.NewV4()).String())) return 1 } -func (n *NakamaModule) uuidBytesToString(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) uuidBytesToString(l *lua.LState) int { uuidBytes := l.CheckString(1) if uuidBytes == "" { l.ArgError(1, "expects a UUID byte string") @@ -510,7 +564,7 @@ func (n *NakamaModule) uuidBytesToString(l *lua.LState) int { return 1 } -func (n *NakamaModule) uuidStringToBytes(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) uuidStringToBytes(l *lua.LState) int { uuidString := l.CheckString(1) if uuidString == "" { l.ArgError(1, "expects a UUID string") @@ -525,7 +579,7 @@ func (n *NakamaModule) uuidStringToBytes(l *lua.LState) int { return 1 } -func (n *NakamaModule) httpRequest(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) httpRequest(l *lua.LState) int { url := l.CheckString(1) method := l.CheckString(2) headers := l.CheckTable(3) @@ -555,7 +609,7 @@ func (n *NakamaModule) httpRequest(l *lua.LState) int { return 0 } // Apply any request headers. - httpHeaders := ConvertLuaTable(headers) + httpHeaders := RuntimeLuaConvertLuaTable(headers) for k, v := range httpHeaders { if vs, ok := v.(string); !ok { l.RaiseError("HTTP header values must be strings") @@ -588,19 +642,19 @@ func (n *NakamaModule) httpRequest(l *lua.LState) int { } l.Push(lua.LNumber(resp.StatusCode)) - l.Push(ConvertMap(l, responseHeaders)) + l.Push(RuntimeLuaConvertMap(l, responseHeaders)) l.Push(lua.LString(string(responseBody))) return 3 } -func (n *NakamaModule) jsonEncode(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) jsonEncode(l *lua.LState) int { value := l.Get(1) if value == nil { l.ArgError(1, "expects a non-nil value to encode") return 0 } - jsonData := ConvertLuaValue(value) + jsonData := RuntimeLuaConvertLuaValue(value) jsonBytes, err := json.Marshal(jsonData) if err != nil { l.RaiseError("error encoding to JSON: %v", err.Error()) @@ -611,7 +665,7 @@ func (n *NakamaModule) jsonEncode(l *lua.LState) int { return 1 } -func (n *NakamaModule) jsonDecode(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) jsonDecode(l *lua.LState) int { jsonString := l.CheckString(1) if jsonString == "" { l.ArgError(1, "expects JSON string") @@ -624,11 +678,11 @@ func (n *NakamaModule) jsonDecode(l *lua.LState) int { return 0 } - l.Push(ConvertValue(l, jsonData)) + l.Push(RuntimeLuaConvertValue(l, jsonData)) return 1 } -func (n *NakamaModule) base64Encode(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) base64Encode(l *lua.LState) int { input := l.CheckString(1) if input == "" { l.ArgError(1, "expects string") @@ -646,7 +700,7 @@ func (n *NakamaModule) base64Encode(l *lua.LState) int { return 1 } -func (n *NakamaModule) base64Decode(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) base64Decode(l *lua.LState) int { input := l.CheckString(1) if input == "" { l.ArgError(1, "expects string") @@ -672,7 +726,7 @@ func (n *NakamaModule) base64Decode(l *lua.LState) int { return 1 } -func (n *NakamaModule) base64URLEncode(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) base64URLEncode(l *lua.LState) int { input := l.CheckString(1) if input == "" { l.ArgError(1, "expects string") @@ -690,7 +744,7 @@ func (n *NakamaModule) base64URLEncode(l *lua.LState) int { return 1 } -func (n *NakamaModule) base64URLDecode(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) base64URLDecode(l *lua.LState) int { input := l.CheckString(1) if input == "" { l.ArgError(1, "expects string") @@ -716,7 +770,7 @@ func (n *NakamaModule) base64URLDecode(l *lua.LState) int { return 1 } -func (n *NakamaModule) base16Encode(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) base16Encode(l *lua.LState) int { input := l.CheckString(1) if input == "" { l.ArgError(1, "expects string") @@ -728,7 +782,7 @@ func (n *NakamaModule) base16Encode(l *lua.LState) int { return 1 } -func (n *NakamaModule) base16Decode(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) base16Decode(l *lua.LState) int { input := l.CheckString(1) if input == "" { l.ArgError(1, "expects string") @@ -745,7 +799,7 @@ func (n *NakamaModule) base16Decode(l *lua.LState) int { return 1 } -func (n *NakamaModule) aes128Encrypt(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) aes128Encrypt(l *lua.LState) int { input := l.CheckString(1) if input == "" { l.ArgError(1, "expects string") @@ -782,7 +836,7 @@ func (n *NakamaModule) aes128Encrypt(l *lua.LState) int { return 1 } -func (n *NakamaModule) aes128Decrypt(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) aes128Decrypt(l *lua.LState) int { input := l.CheckString(1) if input == "" { l.ArgError(1, "expects string") @@ -816,7 +870,7 @@ func (n *NakamaModule) aes128Decrypt(l *lua.LState) int { return 1 } -func (n *NakamaModule) md5Hash(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) md5Hash(l *lua.LState) int { input := l.CheckString(1) if input == "" { l.ArgError(1, "expects input string") @@ -829,7 +883,7 @@ func (n *NakamaModule) md5Hash(l *lua.LState) int { return 1 } -func (n *NakamaModule) sha256Hash(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) sha256Hash(l *lua.LState) int { input := l.CheckString(1) if input == "" { l.ArgError(1, "expects input string") @@ -842,7 +896,7 @@ func (n *NakamaModule) sha256Hash(l *lua.LState) int { return 1 } -func (n *NakamaModule) hmacSHA256Hash(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) hmacSHA256Hash(l *lua.LState) int { input := l.CheckString(1) if input == "" { l.ArgError(1, "expects input string") @@ -865,7 +919,7 @@ func (n *NakamaModule) hmacSHA256Hash(l *lua.LState) int { return 1 } -func (n *NakamaModule) bcryptHash(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) bcryptHash(l *lua.LState) int { input := l.CheckString(1) if input == "" { l.ArgError(1, "expects string") @@ -882,7 +936,7 @@ func (n *NakamaModule) bcryptHash(l *lua.LState) int { return 1 } -func (n *NakamaModule) bcryptCompare(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) bcryptCompare(l *lua.LState) int { hash := l.CheckString(1) if hash == "" { l.ArgError(1, "expects string") @@ -907,7 +961,7 @@ func (n *NakamaModule) bcryptCompare(l *lua.LState) int { return 0 } -func (n *NakamaModule) authenticateCustom(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) authenticateCustom(l *lua.LState) int { // Parse ID. id := l.CheckString(1) if id == "" { @@ -948,7 +1002,7 @@ func (n *NakamaModule) authenticateCustom(l *lua.LState) int { return 3 } -func (n *NakamaModule) authenticateDevice(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) authenticateDevice(l *lua.LState) int { // Parse ID. id := l.CheckString(1) if id == "" { @@ -989,7 +1043,7 @@ func (n *NakamaModule) authenticateDevice(l *lua.LState) int { return 3 } -func (n *NakamaModule) authenticateEmail(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) authenticateEmail(l *lua.LState) int { // Parse email. email := l.CheckString(1) if email == "" { @@ -1045,7 +1099,7 @@ func (n *NakamaModule) authenticateEmail(l *lua.LState) int { return 3 } -func (n *NakamaModule) authenticateFacebook(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) authenticateFacebook(l *lua.LState) int { // Parse access token. token := l.CheckString(1) if token == "" { @@ -1088,7 +1142,7 @@ func (n *NakamaModule) authenticateFacebook(l *lua.LState) int { return 3 } -func (n *NakamaModule) authenticateGameCenter(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) authenticateGameCenter(l *lua.LState) int { // Parse authentication credentials. playerID := l.CheckString(1) if playerID == "" { @@ -1148,7 +1202,7 @@ func (n *NakamaModule) authenticateGameCenter(l *lua.LState) int { return 3 } -func (n *NakamaModule) authenticateGoogle(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) authenticateGoogle(l *lua.LState) int { // Parse ID token. token := l.CheckString(1) if token == "" { @@ -1183,7 +1237,7 @@ func (n *NakamaModule) authenticateGoogle(l *lua.LState) int { return 3 } -func (n *NakamaModule) authenticateSteam(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) authenticateSteam(l *lua.LState) int { if n.config.GetSocial().Steam.PublisherKey == "" || n.config.GetSocial().Steam.AppID == 0 { l.RaiseError("Steam authentication is not configured") return 0 @@ -1223,7 +1277,7 @@ func (n *NakamaModule) authenticateSteam(l *lua.LState) int { return 3 } -func (n *NakamaModule) authenticateTokenGenerate(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) authenticateTokenGenerate(l *lua.LState) int { // Parse input User ID. userIDString := l.CheckString(1) if userIDString == "" { @@ -1249,13 +1303,14 @@ func (n *NakamaModule) authenticateTokenGenerate(l *lua.LState) int { exp = time.Now().UTC().Add(time.Duration(n.config.GetSession().TokenExpirySec) * time.Second).Unix() } - token := generateTokenWithExpiry(n.config, userIDString, username, exp) + token, exp := generateTokenWithExpiry(n.config, userIDString, username, exp) l.Push(lua.LString(token)) - return 1 + l.Push(lua.LNumber(exp)) + return 2 } -func (n *NakamaModule) loggerInfo(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) loggerInfo(l *lua.LState) int { message := l.CheckString(1) if message == "" { l.ArgError(1, "expects message string") @@ -1266,7 +1321,7 @@ func (n *NakamaModule) loggerInfo(l *lua.LState) int { return 1 } -func (n *NakamaModule) loggerWarn(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) loggerWarn(l *lua.LState) int { message := l.CheckString(1) if message == "" { l.ArgError(1, "expects message string") @@ -1277,7 +1332,7 @@ func (n *NakamaModule) loggerWarn(l *lua.LState) int { return 1 } -func (n *NakamaModule) loggerError(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) loggerError(l *lua.LState) int { message := l.CheckString(1) if message == "" { l.ArgError(1, "expects message string") @@ -1288,7 +1343,7 @@ func (n *NakamaModule) loggerError(l *lua.LState) int { return 1 } -func (n *NakamaModule) accountGetId(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) accountGetId(l *lua.LState) int { input := l.CheckString(1) if input == "" { l.ArgError(1, "invalid user id") @@ -1337,7 +1392,7 @@ func (n *NakamaModule) accountGetId(l *lua.LState) int { l.RaiseError(fmt.Sprintf("failed to convert metadata to json: %s", err.Error())) return 0 } - metadataTable := ConvertMap(l, metadataMap) + metadataTable := RuntimeLuaConvertMap(l, metadataMap) accountTable.RawSetString("metadata", metadataTable) walletMap := make(map[string]interface{}) @@ -1346,7 +1401,7 @@ func (n *NakamaModule) accountGetId(l *lua.LState) int { l.RaiseError(fmt.Sprintf("failed to convert wallet to json: %s", err.Error())) return 0 } - walletTable := ConvertMap(l, walletMap) + walletTable := RuntimeLuaConvertMap(l, walletMap) accountTable.RawSetString("wallet", walletTable) if account.Email != "" { @@ -1372,7 +1427,7 @@ func (n *NakamaModule) accountGetId(l *lua.LState) int { return 1 } -func (n *NakamaModule) usersGetId(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) usersGetId(l *lua.LState) int { // Input table validation. input := l.OptTable(1, nil) if input == nil { @@ -1383,7 +1438,7 @@ func (n *NakamaModule) usersGetId(l *lua.LState) int { l.Push(l.CreateTable(0, 0)) return 1 } - userIDs, ok := ConvertLuaValue(input).([]interface{}) + userIDs, ok := RuntimeLuaConvertLuaValue(input).([]interface{}) if !ok { l.ArgError(1, "invalid user id data") return 0 @@ -1448,7 +1503,7 @@ func (n *NakamaModule) usersGetId(l *lua.LState) int { l.RaiseError(fmt.Sprintf("failed to convert metadata to json: %s", err.Error())) return 0 } - metadataTable := ConvertMap(l, metadataMap) + metadataTable := RuntimeLuaConvertMap(l, metadataMap) ut.RawSetString("metadata", metadataTable) usersTable.RawSetInt(i+1, ut) @@ -1458,7 +1513,7 @@ func (n *NakamaModule) usersGetId(l *lua.LState) int { return 1 } -func (n *NakamaModule) usersGetUsername(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) usersGetUsername(l *lua.LState) int { // Input table validation. input := l.OptTable(1, nil) if input == nil { @@ -1469,7 +1524,7 @@ func (n *NakamaModule) usersGetUsername(l *lua.LState) int { l.Push(l.CreateTable(0, 0)) return 1 } - usernames, ok := ConvertLuaValue(input).([]interface{}) + usernames, ok := RuntimeLuaConvertLuaValue(input).([]interface{}) if !ok { l.ArgError(1, "invalid username data") return 0 @@ -1531,7 +1586,7 @@ func (n *NakamaModule) usersGetUsername(l *lua.LState) int { l.RaiseError(fmt.Sprintf("failed to convert metadata to json: %s", err.Error())) return 0 } - metadataTable := ConvertMap(l, metadataMap) + metadataTable := RuntimeLuaConvertMap(l, metadataMap) ut.RawSetString("metadata", metadataTable) usersTable.RawSetInt(i+1, ut) @@ -1541,7 +1596,7 @@ func (n *NakamaModule) usersGetUsername(l *lua.LState) int { return 1 } -func (n *NakamaModule) usersBanId(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) usersBanId(l *lua.LState) int { // Input table validation. input := l.OptTable(1, nil) if input == nil { @@ -1551,7 +1606,7 @@ func (n *NakamaModule) usersBanId(l *lua.LState) int { if input.Len() == 0 { return 0 } - userIDs, ok := ConvertLuaValue(input).([]interface{}) + userIDs, ok := RuntimeLuaConvertLuaValue(input).([]interface{}) if !ok { l.ArgError(1, "invalid user id data") return 0 @@ -1584,7 +1639,7 @@ func (n *NakamaModule) usersBanId(l *lua.LState) int { return 0 } -func (n *NakamaModule) usersUnbanId(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) usersUnbanId(l *lua.LState) int { // Input table validation. input := l.OptTable(1, nil) if input == nil { @@ -1594,7 +1649,7 @@ func (n *NakamaModule) usersUnbanId(l *lua.LState) int { if input.Len() == 0 { return 0 } - userIDs, ok := ConvertLuaValue(input).([]interface{}) + userIDs, ok := RuntimeLuaConvertLuaValue(input).([]interface{}) if !ok { l.ArgError(1, "invalid user id data") return 0 @@ -1627,7 +1682,7 @@ func (n *NakamaModule) usersUnbanId(l *lua.LState) int { return 0 } -func (n *NakamaModule) streamUserList(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) streamUserList(l *lua.LState) int { // Parse input stream identifier. streamTable := l.CheckTable(1) if streamTable == nil { @@ -1704,7 +1759,7 @@ func (n *NakamaModule) streamUserList(l *lua.LState) int { return 1 } -func (n *NakamaModule) streamUserGet(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) streamUserGet(l *lua.LState) int { // Parse input User ID. userIDString := l.CheckString(1) if userIDString == "" { @@ -1803,7 +1858,7 @@ func (n *NakamaModule) streamUserGet(l *lua.LState) int { return 1 } -func (n *NakamaModule) streamUserJoin(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) streamUserJoin(l *lua.LState) int { // Parse input User ID. userIDString := l.CheckString(1) if userIDString == "" { @@ -1918,7 +1973,7 @@ func (n *NakamaModule) streamUserJoin(l *lua.LState) int { return 1 } -func (n *NakamaModule) streamUserUpdate(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) streamUserUpdate(l *lua.LState) int { // Parse input User ID. userIDString := l.CheckString(1) if userIDString == "" { @@ -2030,7 +2085,7 @@ func (n *NakamaModule) streamUserUpdate(l *lua.LState) int { return 0 } -func (n *NakamaModule) streamUserLeave(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) streamUserLeave(l *lua.LState) int { // Parse input User ID. userIDString := l.CheckString(1) if userIDString == "" { @@ -2120,7 +2175,7 @@ func (n *NakamaModule) streamUserLeave(l *lua.LState) int { return 0 } -func (n *NakamaModule) streamCount(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) streamCount(l *lua.LState) int { // Parse input stream identifier. streamTable := l.CheckTable(1) if streamTable == nil { @@ -2187,7 +2242,7 @@ func (n *NakamaModule) streamCount(l *lua.LState) int { return 1 } -func (n *NakamaModule) streamClose(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) streamClose(l *lua.LState) int { // Parse input stream identifier. streamTable := l.CheckTable(1) if streamTable == nil { @@ -2253,7 +2308,7 @@ func (n *NakamaModule) streamClose(l *lua.LState) int { return 0 } -func (n *NakamaModule) streamSend(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) streamSend(l *lua.LState) int { // Parse input stream identifier. streamTable := l.CheckTable(1) if streamTable == nil { @@ -2337,7 +2392,7 @@ func (n *NakamaModule) streamSend(l *lua.LState) int { return 0 } -func (n *NakamaModule) matchCreate(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) matchCreate(l *lua.LState) int { // Parse the name of the Lua module that should handle the match. name := l.CheckString(1) if name == "" { @@ -2345,10 +2400,31 @@ func (n *NakamaModule) matchCreate(l *lua.LState) int { return 0 } - params := ConvertLuaValue(l.Get(2)) + params := RuntimeLuaConvertLuaValue(l.Get(2)) + var paramsMap map[string]interface{} + if params != nil { + var ok bool + paramsMap, ok = params.(map[string]interface{}) + if !ok { + l.ArgError(2, "expects params to be nil or a table of key-value pairs") + return 0 + } + } + + id := uuid.Must(uuid.NewV4()) + matchLogger := n.logger.With(zap.String("mid", id.String())) + label := atomic.NewString("") + labelUpdateFn := func(input string) { + label.Store(input) + } + core, err := n.matchCreateFn(matchLogger, id, n.node, name, labelUpdateFn) + if err != nil { + l.RaiseError("error creating match: %v", err.Error()) + return 0 + } // Start the match. - mh, err := n.matchRegistry.NewMatch(name, params) + mh, err := n.matchRegistry.NewMatch(matchLogger, id, label, core, paramsMap) if err != nil { l.RaiseError("error creating match: %v", err.Error()) return 0 @@ -2359,7 +2435,7 @@ func (n *NakamaModule) matchCreate(l *lua.LState) int { return 1 } -func (n *NakamaModule) matchList(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) matchList(l *lua.LState) int { // Parse limit. limit := l.OptInt(1, 1) @@ -2422,7 +2498,7 @@ func (n *NakamaModule) matchList(l *lua.LState) int { return 1 } -func (n *NakamaModule) notificationSend(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) notificationSend(l *lua.LState) int { u := l.CheckString(1) userID, err := uuid.FromString(u) if err != nil { @@ -2436,7 +2512,7 @@ func (n *NakamaModule) notificationSend(l *lua.LState) int { return 0 } - contentMap := ConvertLuaTable(l.CheckTable(3)) + contentMap := RuntimeLuaConvertLuaTable(l.CheckTable(3)) contentBytes, err := json.Marshal(contentMap) if err != nil { l.ArgError(1, fmt.Sprintf("failed to convert content: %s", err.Error())) @@ -2455,7 +2531,7 @@ func (n *NakamaModule) notificationSend(l *lua.LState) int { if s != "" { suid, err := uuid.FromString(s) if err != nil { - l.ArgError(5, "expects sender)id to either be not set, empty string or a valid UUID") + l.ArgError(5, "expects sender_id to either be not set, empty string or a valid UUID") return 0 } senderID = suid.String() @@ -2483,7 +2559,7 @@ func (n *NakamaModule) notificationSend(l *lua.LState) int { return 0 } -func (n *NakamaModule) notificationsSend(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) notificationsSend(l *lua.LState) int { notificationsTable := l.CheckTable(1) if notificationsTable == nil { l.ArgError(1, "expects a valid set of notifications") @@ -2534,7 +2610,7 @@ func (n *NakamaModule) notificationsSend(l *lua.LState) int { return } - contentMap := ConvertLuaTable(v.(*lua.LTable)) + contentMap := RuntimeLuaConvertLuaTable(v.(*lua.LTable)) contentBytes, err := json.Marshal(contentMap) if err != nil { conversionError = true @@ -2636,7 +2712,7 @@ func (n *NakamaModule) notificationsSend(l *lua.LState) int { return 0 } -func (n *NakamaModule) walletUpdate(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) walletUpdate(l *lua.LState) int { // Parse user ID. uid := l.CheckString(1) if uid == "" { @@ -2655,13 +2731,13 @@ func (n *NakamaModule) walletUpdate(l *lua.LState) int { l.ArgError(2, "expects a table as changeset value") return 0 } - changesetMap := ConvertLuaTable(changesetTable) + changesetMap := RuntimeLuaConvertLuaTable(changesetTable) // Parse metadata, optional. metadataBytes := []byte("{}") metadataTable := l.OptTable(3, nil) if metadataTable != nil { - metadataMap := ConvertLuaTable(metadataTable) + metadataMap := RuntimeLuaConvertLuaTable(metadataTable) metadataBytes, err = json.Marshal(metadataMap) if err != nil { l.ArgError(3, fmt.Sprintf("failed to convert metadata: %s", err.Error())) @@ -2679,7 +2755,7 @@ func (n *NakamaModule) walletUpdate(l *lua.LState) int { return 0 } -func (n *NakamaModule) walletsUpdate(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) walletsUpdate(l *lua.LState) int { updatesTable := l.CheckTable(1) if updatesTable == nil { l.ArgError(1, "expects a valid set of updates") @@ -2730,14 +2806,14 @@ func (n *NakamaModule) walletsUpdate(l *lua.LState) int { l.ArgError(1, "expects changeset to be table") return } - update.Changeset = ConvertLuaTable(v.(*lua.LTable)) + update.Changeset = RuntimeLuaConvertLuaTable(v.(*lua.LTable)) case "metadata": if v.Type() != lua.LTTable { conversionError = true l.ArgError(1, "expects metadata to be table") return } - metadataMap := ConvertLuaTable(v.(*lua.LTable)) + metadataMap := RuntimeLuaConvertLuaTable(v.(*lua.LTable)) metadataBytes, err := json.Marshal(metadataMap) if err != nil { conversionError = true @@ -2775,7 +2851,7 @@ func (n *NakamaModule) walletsUpdate(l *lua.LState) int { return 0 } -func (n *NakamaModule) walletLedgerUpdate(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) walletLedgerUpdate(l *lua.LState) int { // Parse ledger ID. id := l.CheckString(1) if id == "" { @@ -2794,7 +2870,7 @@ func (n *NakamaModule) walletLedgerUpdate(l *lua.LState) int { l.ArgError(2, "expects a table as metadata value") return 0 } - metadataMap := ConvertLuaTable(metadataTable) + metadataMap := RuntimeLuaConvertLuaTable(metadataTable) metadataBytes, err := json.Marshal(metadataMap) if err != nil { l.ArgError(2, fmt.Sprintf("failed to convert metadata: %s", err.Error())) @@ -2804,6 +2880,7 @@ func (n *NakamaModule) walletLedgerUpdate(l *lua.LState) int { item, err := UpdateWalletLedger(n.logger, n.db, itemID, string(metadataBytes)) if err != nil { l.RaiseError(fmt.Sprintf("failed to update user wallet ledger: %s", err.Error())) + return 0 } itemTable := l.CreateTable(0, 6) @@ -2812,7 +2889,7 @@ func (n *NakamaModule) walletLedgerUpdate(l *lua.LState) int { itemTable.RawSetString("create_time", lua.LNumber(item.CreateTime)) itemTable.RawSetString("update_time", lua.LNumber(item.UpdateTime)) - changesetTable := ConvertMap(l, item.Changeset) + changesetTable := RuntimeLuaConvertMap(l, item.Changeset) itemTable.RawSetString("changeset", changesetTable) itemTable.RawSetString("metadata", metadataTable) @@ -2821,7 +2898,7 @@ func (n *NakamaModule) walletLedgerUpdate(l *lua.LState) int { return 1 } -func (n *NakamaModule) walletLedgerList(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) walletLedgerList(l *lua.LState) int { // Parse user ID. uid := l.CheckString(1) if uid == "" { @@ -2848,10 +2925,10 @@ func (n *NakamaModule) walletLedgerList(l *lua.LState) int { itemTable.RawSetString("create_time", lua.LNumber(item.CreateTime)) itemTable.RawSetString("update_time", lua.LNumber(item.UpdateTime)) - changesetTable := ConvertMap(l, item.Changeset) + changesetTable := RuntimeLuaConvertMap(l, item.Changeset) itemTable.RawSetString("changeset", changesetTable) - metadataTable := ConvertMap(l, item.Metadata) + metadataTable := RuntimeLuaConvertMap(l, item.Metadata) itemTable.RawSetString("metadata", metadataTable) itemsTable.RawSetInt(i+1, itemTable) @@ -2861,7 +2938,7 @@ func (n *NakamaModule) walletLedgerList(l *lua.LState) int { return 1 } -func (n *NakamaModule) storageList(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) storageList(l *lua.LState) int { userIDString := l.OptString(1, "") collection := l.OptString(2, "") limit := l.CheckInt(3) @@ -2905,7 +2982,7 @@ func (n *NakamaModule) storageList(l *lua.LState) int { l.RaiseError(fmt.Sprintf("failed to convert value to json: %s", err.Error())) return 0 } - valueTable := ConvertMap(l, valueMap) + valueTable := RuntimeLuaConvertMap(l, valueMap) vt.RawSetString("value", valueTable) lv.RawSetInt(i+1, vt) @@ -2921,7 +2998,7 @@ func (n *NakamaModule) storageList(l *lua.LState) int { return 2 } -func (n *NakamaModule) storageRead(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) storageRead(l *lua.LState) int { keysTable := l.CheckTable(1) if keysTable == nil { l.ArgError(1, "expects a valid set of keys") @@ -3048,7 +3125,7 @@ func (n *NakamaModule) storageRead(l *lua.LState) int { l.RaiseError(fmt.Sprintf("failed to convert value to json: %s", err.Error())) return 0 } - valueTable := ConvertMap(l, valueMap) + valueTable := RuntimeLuaConvertMap(l, valueMap) vt.RawSetString("value", valueTable) lv.RawSetInt(i+1, vt) @@ -3057,7 +3134,7 @@ func (n *NakamaModule) storageRead(l *lua.LState) int { return 1 } -func (n *NakamaModule) storageWrite(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) storageWrite(l *lua.LState) int { dataTable := l.CheckTable(1) if dataTable == nil { l.ArgError(1, "expects a valid set of data") @@ -3134,7 +3211,7 @@ func (n *NakamaModule) storageWrite(l *lua.LState) int { l.ArgError(1, "expects value to be table") return } - valueMap := ConvertLuaTable(v.(*lua.LTable)) + valueMap := RuntimeLuaConvertLuaTable(v.(*lua.LTable)) valueBytes, err := json.Marshal(valueMap) if err != nil { conversionError = true @@ -3232,7 +3309,7 @@ func (n *NakamaModule) storageWrite(l *lua.LState) int { return 1 } -func (n *NakamaModule) storageDelete(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) storageDelete(l *lua.LState) int { keysTable := l.CheckTable(1) if keysTable == nil { l.ArgError(1, "expects a valid set of object IDs") @@ -3348,7 +3425,7 @@ func (n *NakamaModule) storageDelete(l *lua.LState) int { return 0 } -func (n *NakamaModule) leaderboardCreate(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) leaderboardCreate(l *lua.LState) int { id := l.CheckString(1) if id == "" { l.ArgError(1, "expects a leaderboard ID string") @@ -3394,7 +3471,7 @@ func (n *NakamaModule) leaderboardCreate(l *lua.LState) int { metadata := l.OptTable(6, nil) metadataStr := "{}" if metadata != nil { - metadataMap := ConvertLuaTable(metadata) + metadataMap := RuntimeLuaConvertLuaTable(metadata) metadataBytes, err := json.Marshal(metadataMap) if err != nil { l.RaiseError("error encoding metadata: %v", err.Error()) @@ -3409,7 +3486,7 @@ func (n *NakamaModule) leaderboardCreate(l *lua.LState) int { return 0 } -func (n *NakamaModule) leaderboardDelete(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) leaderboardDelete(l *lua.LState) int { id := l.CheckString(1) if id == "" { l.ArgError(1, "expects a leaderboard ID string") @@ -3422,7 +3499,7 @@ func (n *NakamaModule) leaderboardDelete(l *lua.LState) int { return 0 } -func (n *NakamaModule) leaderboardRecordsList(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) leaderboardRecordsList(l *lua.LState) int { id := l.CheckString(1) if id == "" { l.ArgError(1, "expects a leaderboard ID string") @@ -3501,7 +3578,7 @@ func (n *NakamaModule) leaderboardRecordsList(l *lua.LState) int { l.RaiseError(fmt.Sprintf("failed to convert metadata to json: %s", err.Error())) return 0 } - metadataTable := ConvertMap(l, metadataMap) + metadataTable := RuntimeLuaConvertMap(l, metadataMap) recordTable.RawSetString("metadata", metadataTable) recordTable.RawSetString("create_time", lua.LNumber(record.CreateTime.Seconds)) @@ -3537,7 +3614,7 @@ func (n *NakamaModule) leaderboardRecordsList(l *lua.LState) int { l.RaiseError(fmt.Sprintf("failed to convert metadata to json: %s", err.Error())) return 0 } - metadataTable := ConvertMap(l, metadataMap) + metadataTable := RuntimeLuaConvertMap(l, metadataMap) recordTable.RawSetString("metadata", metadataTable) recordTable.RawSetString("create_time", lua.LNumber(record.CreateTime.Seconds)) @@ -3568,7 +3645,7 @@ func (n *NakamaModule) leaderboardRecordsList(l *lua.LState) int { return 4 } -func (n *NakamaModule) leaderboardRecordWrite(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) leaderboardRecordWrite(l *lua.LState) int { id := l.CheckString(1) if id == "" { l.ArgError(1, "expects a leaderboard ID string") @@ -3598,7 +3675,7 @@ func (n *NakamaModule) leaderboardRecordWrite(l *lua.LState) int { metadata := l.OptTable(6, nil) metadataStr := "" if metadata != nil { - metadataMap := ConvertLuaTable(metadata) + metadataMap := RuntimeLuaConvertLuaTable(metadata) metadataBytes, err := json.Marshal(metadataMap) if err != nil { l.RaiseError("error encoding metadata: %v", err.Error()) @@ -3631,7 +3708,7 @@ func (n *NakamaModule) leaderboardRecordWrite(l *lua.LState) int { l.RaiseError(fmt.Sprintf("failed to convert metadata to json: %s", err.Error())) return 0 } - metadataTable := ConvertMap(l, metadataMap) + metadataTable := RuntimeLuaConvertMap(l, metadataMap) recordTable.RawSetString("metadata", metadataTable) recordTable.RawSetString("create_time", lua.LNumber(record.CreateTime.Seconds)) @@ -3646,7 +3723,7 @@ func (n *NakamaModule) leaderboardRecordWrite(l *lua.LState) int { return 1 } -func (n *NakamaModule) leaderboardRecordDelete(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) leaderboardRecordDelete(l *lua.LState) int { id := l.CheckString(1) if id == "" { l.ArgError(1, "expects a leaderboard ID string") @@ -3665,7 +3742,7 @@ func (n *NakamaModule) leaderboardRecordDelete(l *lua.LState) int { return 0 } -func (n *NakamaModule) groupCreate(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) groupCreate(l *lua.LState) int { userID, err := uuid.FromString(l.CheckString(1)) if err != nil { l.ArgError(1, "expects user ID to be a valid identifier") @@ -3691,7 +3768,7 @@ func (n *NakamaModule) groupCreate(l *lua.LState) int { metadata := l.OptTable(8, nil) metadataStr := "" if metadata != nil { - metadataMap := ConvertLuaTable(metadata) + metadataMap := RuntimeLuaConvertLuaTable(metadata) metadataBytes, err := json.Marshal(metadataMap) if err != nil { l.RaiseError("error encoding metadata: %v", err.Error()) @@ -3730,7 +3807,7 @@ func (n *NakamaModule) groupCreate(l *lua.LState) int { l.RaiseError(fmt.Sprintf("failed to convert metadata to json: %s", err.Error())) return 0 } - metadataTable := ConvertMap(l, metadataMap) + metadataTable := RuntimeLuaConvertMap(l, metadataMap) groupTable.RawSetString("metadata", metadataTable) groupTable.RawSetString("open", lua.LBool(group.Open.Value)) groupTable.RawSetString("edge_count", lua.LNumber(group.EdgeCount)) @@ -3742,7 +3819,7 @@ func (n *NakamaModule) groupCreate(l *lua.LState) int { return 1 } -func (n *NakamaModule) groupUpdate(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) groupUpdate(l *lua.LState) int { groupID, err := uuid.FromString(l.CheckString(1)) if err != nil { l.ArgError(1, "expects group ID to be a valid identifier") @@ -3793,7 +3870,7 @@ func (n *NakamaModule) groupUpdate(l *lua.LState) int { metadataTable := l.OptTable(8, nil) var metadata *wrappers.StringValue if metadataTable != nil { - metadataMap := ConvertLuaTable(metadataTable) + metadataMap := RuntimeLuaConvertLuaTable(metadataTable) metadataBytes, err := json.Marshal(metadataMap) if err != nil { l.RaiseError("error encoding metadata: %v", err.Error()) @@ -3816,7 +3893,7 @@ func (n *NakamaModule) groupUpdate(l *lua.LState) int { return 0 } -func (n *NakamaModule) groupDelete(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) groupDelete(l *lua.LState) int { groupID, err := uuid.FromString(l.CheckString(1)) if err != nil { l.ArgError(1, "expects group ID to be a valid identifier") @@ -3831,7 +3908,7 @@ func (n *NakamaModule) groupDelete(l *lua.LState) int { return 0 } -func (n *NakamaModule) groupUsersList(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) groupUsersList(l *lua.LState) int { groupID, err := uuid.FromString(l.CheckString(1)) if err != nil { l.ArgError(1, "expects group ID to be a valid identifier") @@ -3879,7 +3956,7 @@ func (n *NakamaModule) groupUsersList(l *lua.LState) int { l.RaiseError(fmt.Sprintf("failed to convert metadata to json: %s", err.Error())) return 0 } - metadataTable := ConvertMap(l, metadataMap) + metadataTable := RuntimeLuaConvertMap(l, metadataMap) ut.RawSetString("metadata", metadataTable) gt := l.CreateTable(0, 2) @@ -3893,7 +3970,7 @@ func (n *NakamaModule) groupUsersList(l *lua.LState) int { return 1 } -func (n *NakamaModule) userGroupsList(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) userGroupsList(l *lua.LState) int { userID, err := uuid.FromString(l.CheckString(1)) if err != nil { l.ArgError(1, "expects user ID to be a valid identifier") @@ -3929,7 +4006,7 @@ func (n *NakamaModule) userGroupsList(l *lua.LState) int { l.RaiseError(fmt.Sprintf("failed to convert metadata to json: %s", err.Error())) return 0 } - metadataTable := ConvertMap(l, metadataMap) + metadataTable := RuntimeLuaConvertMap(l, metadataMap) gt.RawSetString("metadata", metadataTable) ugt := l.CreateTable(0, 2) @@ -3943,7 +4020,7 @@ func (n *NakamaModule) userGroupsList(l *lua.LState) int { return 1 } -func (n *NakamaModule) accountUpdateId(l *lua.LState) int { +func (n *RuntimeLuaNakamaModule) accountUpdateId(l *lua.LState) int { userID, err := uuid.FromString(l.CheckString(1)) if err != nil { l.ArgError(1, "expects user ID to be a valid identifier") @@ -3953,7 +4030,7 @@ func (n *NakamaModule) accountUpdateId(l *lua.LState) int { metadataTable := l.OptTable(2, nil) var metadata *wrappers.StringValue if metadataTable != nil { - metadataMap := ConvertLuaTable(metadataTable) + metadataMap := RuntimeLuaConvertLuaTable(metadataTable) metadataBytes, err := json.Marshal(metadataMap) if err != nil { l.RaiseError("error encoding metadata: %v", err.Error()) diff --git a/server/runtime_oslib.go b/server/runtime_lua_oslib.go similarity index 100% rename from server/runtime_oslib.go rename to server/runtime_lua_oslib.go diff --git a/server/runtime_utils.go b/server/runtime_lua_utils.go similarity index 100% rename from server/runtime_utils.go rename to server/runtime_lua_utils.go diff --git a/server/runtime_module_cache.go b/server/runtime_module_cache.go deleted file mode 100644 index 3df54c2b88ab992628b2451808d767cfe7701a84..0000000000000000000000000000000000000000 --- a/server/runtime_module_cache.go +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright 2018 The Nakama Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package server - -import ( - "database/sql" - "io/ioutil" - "os" - "path/filepath" - "strings" - "sync" - - "github.com/heroiclabs/nakama/social" - "github.com/yuin/gopher-lua" - "go.uber.org/zap" - "sort" -) - -type ModuleCache struct { - Names []string - Modules map[string]*RuntimeModule -} - -func (mc *ModuleCache) Add(m *RuntimeModule) { - mc.Names = append(mc.Names, m.Name) - mc.Modules[m.Name] = m - - // Ensure modules will be listed in ascending order of names. - sort.Strings(mc.Names) -} - -type RegCallbacks struct { - RPC map[string]interface{} - Before map[string]interface{} - After map[string]interface{} - Matchmaker interface{} -} - -func LoadRuntimeModules(startupLogger *zap.Logger, config Config) (map[string]lua.LGFunction, *ModuleCache, error) { - runtimeConfig := config.GetRuntime() - if err := os.MkdirAll(runtimeConfig.Path, os.ModePerm); err != nil { - return nil, nil, err - } - - moduleCache := &ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*RuntimeModule, 0), - } - - // Override before Package library is invoked. - lua.LuaLDir = runtimeConfig.Path - lua.LuaPathDefault = lua.LuaLDir + string(os.PathSeparator) + "?.lua;" + lua.LuaLDir + string(os.PathSeparator) + "?" + string(os.PathSeparator) + "init.lua" - os.Setenv(lua.LuaPath, lua.LuaPathDefault) - - startupLogger.Info("Initialising runtime", zap.String("path", lua.LuaLDir)) - modulePaths := make([]string, 0) - if err := filepath.Walk(lua.LuaLDir, func(path string, f os.FileInfo, err error) error { - if err != nil { - startupLogger.Error("Could not read module", zap.Error(err)) - return err - } else if !f.IsDir() { - if strings.ToLower(filepath.Ext(path)) == ".lua" { - var content []byte - if content, err = ioutil.ReadFile(path); err != nil { - startupLogger.Error("Could not read module", zap.String("path", path), zap.Error(err)) - return err - } - relPath, _ := filepath.Rel(lua.LuaLDir, path) - name := strings.TrimSuffix(relPath, filepath.Ext(relPath)) - // Make paths Lua friendly. - name = strings.Replace(name, string(os.PathSeparator), ".", -1) - moduleCache.Add(&RuntimeModule{ - Name: name, - Path: path, - Content: content, - }) - modulePaths = append(modulePaths, relPath) - } - } - return nil - }); err != nil { - startupLogger.Error("Failed to list modules", zap.Error(err)) - return nil, nil, err - } - - stdLibs := map[string]lua.LGFunction{ - lua.LoadLibName: OpenPackage(moduleCache), - lua.BaseLibName: lua.OpenBase, - lua.TabLibName: lua.OpenTable, - lua.OsLibName: OpenOs, - lua.StringLibName: lua.OpenString, - lua.MathLibName: lua.OpenMath, - Bit32LibName: OpenBit32, - } - - startupLogger.Info("Found runtime modules", zap.Int("count", len(modulePaths)), zap.Strings("modules", modulePaths)) - - return stdLibs, moduleCache, nil -} - -func ValidateRuntimeModules(logger, startupLogger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, leaderboardCache LeaderboardCache, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter, stdLibs map[string]lua.LGFunction, moduleCache *ModuleCache, once *sync.Once) (*RegCallbacks, error) { - regCallbacks := &RegCallbacks{ - RPC: make(map[string]interface{}), - Before: make(map[string]interface{}), - After: make(map[string]interface{}), - } - - startupLogger.Info("Evaluating runtime modules") - r, err := newVM(logger, db, config, socialClient, leaderboardCache, sessionRegistry, matchRegistry, tracker, router, stdLibs, moduleCache, once, func(execMode ExecutionMode, id string) { - switch execMode { - case ExecutionModeRPC: - regCallbacks.RPC[id] = struct{}{} - logger.Info("Registered RPC function invocation", zap.String("id", id)) - case ExecutionModeBefore: - regCallbacks.Before[id] = struct{}{} - logger.Info("Registered Before function invocation", zap.String("id", strings.TrimPrefix(strings.TrimPrefix(id, API_PREFIX), RTAPI_PREFIX))) - case ExecutionModeAfter: - regCallbacks.After[id] = struct{}{} - logger.Info("Registered After function invocation", zap.String("id", strings.TrimPrefix(strings.TrimPrefix(id, API_PREFIX), RTAPI_PREFIX))) - case ExecutionModeMatchmaker: - regCallbacks.Matchmaker = struct{}{} - logger.Info("Registered Matchmaker Matched function invocation") - } - }) - if err != nil { - return nil, err - } - startupLogger.Info("Runtime modules loaded") - r.Stop() - - return regCallbacks, nil -} diff --git a/server/tracker.go b/server/tracker.go index e819cfa5c9bebcf8854bec666508fc31034bec02..b0b3e9a8e52f663e90f920fca823022c22182fac 100644 --- a/server/tracker.go +++ b/server/tracker.go @@ -58,6 +58,19 @@ type PresenceMeta struct { Status string } +func (pm *PresenceMeta) GetHidden() bool { + return pm.Hidden +} +func (pm *PresenceMeta) GetPersistence() bool { + return pm.Persistence +} +func (pm *PresenceMeta) GetUsername() string { + return pm.Username +} +func (pm *PresenceMeta) GetStatus() string { + return pm.Status +} + type Presence struct { ID PresenceID Stream PresenceStream @@ -65,6 +78,28 @@ type Presence struct { Meta PresenceMeta } +func (p *Presence) GetUserId() string { + return p.UserID.String() +} +func (p *Presence) GetSessionId() string { + return p.ID.SessionID.String() +} +func (p *Presence) GetNodeId() string { + return p.ID.Node +} +func (p *Presence) GetHidden() bool { + return p.Meta.Hidden +} +func (p *Presence) GetPersistence() bool { + return p.Meta.Persistence +} +func (p *Presence) GetUsername() string { + return p.Meta.Username +} +func (p *Presence) GetStatus() string { + return p.Meta.Status +} + type PresenceEvent struct { Joins []Presence Leaves []Presence diff --git a/social/social.go b/social/social.go index 57b8bdd8228eb8a2ac16352fb14a17efb13fd74e..ec1071eaa050bc80b24e3d63c187b0d5bf161479 100644 --- a/social/social.go +++ b/social/social.go @@ -403,7 +403,7 @@ func (c *Client) CheckGameCenterID(playerID string, bundleID string, timestamp i // Parse the public key, check issuer, check signature. pubBlock, rest := pem.Decode([]byte(body)) if pubBlock == nil { - pubBlock, rest = pem.Decode([]byte("\n-----BEGIN CERTIFICATE-----\n" + base64.StdEncoding.EncodeToString(rest) + "\n-----END CERTIFICATE-----")) + pubBlock, _ = pem.Decode([]byte("\n-----BEGIN CERTIFICATE-----\n" + base64.StdEncoding.EncodeToString(rest) + "\n-----END CERTIFICATE-----")) if pubBlock == nil { return false, errors.New("gamecenter check error: error decoding public key") } diff --git a/tests/core_storage_test.go b/tests/core_storage_test.go index de0930d6cb1e9ea6a853288c4df1dc2ce9dc1461..c31475963d27269133eb48270b3d09a06abcc59c 100644 --- a/tests/core_storage_test.go +++ b/tests/core_storage_test.go @@ -381,6 +381,9 @@ func TestStorageWritePipelineUserMultiple(t *testing.T) { PermissionWrite: &wrappers.Int32Value{Value: 1}, }}} allAcks, code, err := server.StorageWriteObjects(logger, db, false, data) + if err != nil { + t.Fatal(err.Error()) + } acks := allAcks.Acks assert.Nil(t, err, "err was not nil") diff --git a/tests/runtime_test.go b/tests/runtime_test.go index 19a88283643e0844a3ca48d0810fb7a2b876e1b1..dd0a899d7f4c7f60e2f9c7a2d5efb0f2f0a6a21d 100644 --- a/tests/runtime_test.go +++ b/tests/runtime_test.go @@ -18,54 +18,24 @@ import ( "errors" "io/ioutil" "net/http" + "os" + "path/filepath" "strings" "testing" "fmt" - "sync" "github.com/gofrs/uuid" "github.com/golang/protobuf/ptypes/empty" "github.com/heroiclabs/nakama/api" "github.com/heroiclabs/nakama/rtapi" "github.com/heroiclabs/nakama/server" - "github.com/yuin/gopher-lua" "golang.org/x/crypto/bcrypt" ) -func vm(t *testing.T, moduleCache *server.ModuleCache) *server.RuntimePool { - stdLibs := map[string]lua.LGFunction{ - lua.LoadLibName: server.OpenPackage(moduleCache), - lua.BaseLibName: lua.OpenBase, - lua.TabLibName: lua.OpenTable, - lua.OsLibName: server.OpenOs, - lua.StringLibName: lua.OpenString, - lua.MathLibName: lua.OpenMath, - server.Bit32LibName: server.OpenBit32, - } - - db := NewDB(t) - once := &sync.Once{} - router := &DummyMessageRouter{} - regCallbacks, err := server.ValidateRuntimeModules(logger, logger, db, config, nil, nil, nil, nil, nil, router, stdLibs, moduleCache, once) - if err != nil { - t.Fatalf("Failed initializing runtime modules: %s", err.Error()) - } - - return server.NewRuntimePool(logger, logger, db, config, nil, nil, nil, nil, nil, router, stdLibs, moduleCache, regCallbacks, once) -} - -func writeLuaModule(modules *server.ModuleCache, name, content string) { - modules.Add(&server.RuntimeModule{ - Name: name, - Path: fmt.Sprintf("%v.lua", name), - Content: []byte(content), - }) -} - -func writeStatsModule(modules *server.ModuleCache) { - writeLuaModule(modules, "stats", ` -stats={} +const ( + STATS_MODULE_NAME = "stats" + STATS_MODULE_DATA = `stats={} -- Get the mean value of a table function stats.mean( t ) local sum = 0 @@ -79,64 +49,67 @@ function stats.mean( t ) return (sum / count) end print("Stats Module Loaded") -return stats`) -} - -func writeTestModule(modules *server.ModuleCache) { - writeLuaModule(modules, "test", ` -test={} +return stats` + TEST_MODULE_NAME = "test" + TEST_MODULE_DATA = `test={} -- Get the mean value of a table function test.printWorld() print("Hello World") return {["message"]="Hello World"} end print("Test Module Loaded") -return test -`) +return test` +) + +func runtimeWithModules(t *testing.T, modules map[string]string) (*server.Runtime, error) { + dir, err := ioutil.TempDir("", fmt.Sprintf("nakama_runtime_lua_test_%v", uuid.Must(uuid.NewV4()).String())) + if err != nil { + t.Fatalf("Failed initializing runtime modules tempdir: %s", err.Error()) + } + defer os.RemoveAll(dir) + + for moduleName, moduleData := range modules { + if err := ioutil.WriteFile(filepath.Join(dir, fmt.Sprintf("%v.lua", moduleName)), []byte(moduleData), 0644); err != nil { + t.Fatalf("Failed initializing runtime modules tempfile: %s", err.Error()) + } + } + + cfg := server.NewConfig(logger) + cfg.Runtime.Path = dir + + return server.NewRuntime(logger, logger, NewDB(t), jsonpbMarshaler, jsonpbUnmarshaler, cfg, nil, nil, nil, nil, nil, &DummyMessageRouter{}) } func TestRuntimeSampleScript(t *testing.T) { - rp := vm(t, &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - }) - r := rp.Get() - defer r.Stop() - - l, _ := r.NewStateThread() - defer l.Close() - err := l.DoString(` + modules := map[string]string{ + "mod": ` local example = "an example string" for i in string.gmatch(example, "%S+") do print(i) -end`) +end`, + } + _, err := runtimeWithModules(t, modules) if err != nil { - t.Error(err) + t.Fatal(err) } } func TestRuntimeDisallowStandardLibs(t *testing.T) { - rp := vm(t, &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - }) - r := rp.Get() - defer r.Stop() - - l, _ := r.NewStateThread() - defer l.Close() - err := l.DoString(` + modules := map[string]string{ + "mod": ` -- Return true if file exists and is readable. function file_exists(path) local file = io.open(path, "r") if file then file:close() end return file ~= nil end -file_exists "./"`) +file_exists "./"`, + } + _, err := runtimeWithModules(t, modules) if err == nil { - t.Error(errors.New("successfully accessed IO package")) + t.Fatal(errors.New("successfully accessed IO package")) } } @@ -144,56 +117,53 @@ file_exists "./"`) // Have a look at the stdout messages to see if the module was loaded multiple times // You should only see "Test Module Loaded" once func TestRuntimeRequireEval(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - writeTestModule(modules) - writeLuaModule(modules, "test-invoke", ` + modules := map[string]string{ + TEST_MODULE_NAME: TEST_MODULE_DATA, + "test-invoke": ` local nakama = require("nakama") local test = require("test") -test.printWorld() -`) +test.printWorld()`, + } - vm(t, modules) + _, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } } func TestRuntimeRequireFile(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - writeStatsModule(modules) - writeLuaModule(modules, "local_test", ` + modules := map[string]string{ + STATS_MODULE_NAME: STATS_MODULE_DATA, + "local_test": ` local stats = require("stats") t = {[1]=5, [2]=7, [3]=8, [4]='Something else.'} -assert(stats.mean(t) > 0) -`) +assert(stats.mean(t) > 0)`, + } - vm(t, modules) + _, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } } func TestRuntimeRequirePreload(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - writeStatsModule(modules) - writeLuaModule(modules, "states-invoke", ` + modules := map[string]string{ + STATS_MODULE_NAME: STATS_MODULE_DATA, + "states-invoke": ` local stats = require("stats") t = {[1]=5, [2]=7, [3]=8, [4]='Something else.'} -print(stats.mean(t)) -`) +print(stats.mean(t))`, + } - vm(t, modules) + _, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } } func TestRuntimeBit32(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - writeLuaModule(modules, "bit32-tests", ` + modules := map[string]string{ + "bit32-tests": ` --[[ Original under MIT license at https://github.com/Shopify/lua-tests/blob/master/bitwise.lua --]] @@ -312,18 +282,18 @@ assert(bit32.replace(-1, 0, 31) == 2^31 - 1) assert(bit32.replace(-1, 0, 1, 2) == 2^32 - 7) -print'OK' -`) +print'OK'`, + } - vm(t, modules) + _, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } } func TestRuntimeRegisterRPCWithPayload(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - writeLuaModule(modules, "test", ` + modules := map[string]string{ + "test": ` test={} -- Get the mean value of a table function test.printWorld(ctx, payload) @@ -332,37 +302,37 @@ function test.printWorld(ctx, payload) return payload end print("Test Module Loaded") -return test - `) - writeLuaModule(modules, "http-invoke", ` +return test`, + "http-invoke": ` local nakama = require("nakama") local test = require("test") -nakama.register_rpc(test.printWorld, "helloworld") - `) +nakama.register_rpc(test.printWorld, "helloworld")`, + } - rp := vm(t, modules) - r := rp.Get() - defer r.Stop() + runtime, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } - fn := r.GetCallback(server.ExecutionModeRPC, "helloworld") - payload := "Hello World" + fn := runtime.Rpc("helloworld") + if fn == nil { + t.Fatal("Expected RPC function to be registered") + } - m, err, _ := r.InvokeFunction(server.ExecutionModeRPC, fn, nil, "", "", 0, "", "", "", payload) + payload := "Hello World" + result, err, _ := fn(nil, "", "", 0, "", "", "", payload) if err != nil { - t.Error(err) + t.Fatal(err.Error()) } - if m != payload { - t.Error("Invocation failed. Return result not expected") + if result != payload { + t.Fatal("Invocation failed. Return result not expected") } } func TestRuntimeRegisterRPCWithPayloadEndToEnd(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - writeLuaModule(modules, "test", ` + modules := map[string]string{ + "test": ` test={} -- Get the mean value of a table function test.printWorld(ctx, payload) @@ -371,19 +341,21 @@ function test.printWorld(ctx, payload) return payload end print("Test Module Loaded") -return test - `) - writeLuaModule(modules, "http-invoke", ` +return test`, + "http-invoke": ` local nakama = require("nakama") local test = require("test") -nakama.register_rpc(test.printWorld, "helloworld") - `) +nakama.register_rpc(test.printWorld, "helloworld")`, + } - rp := vm(t, modules) + runtime, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } db := NewDB(t) - pipeline := server.NewPipeline(config, db, jsonpbMarshaler, jsonpbUnmarshaler, nil, nil, nil, nil, nil, rp) - apiServer := server.StartApiServer(logger, logger, db, jsonpbMarshaler, jsonpbUnmarshaler, config, nil, nil, nil, nil, nil, nil, nil, pipeline, rp) + pipeline := server.NewPipeline(logger, config, db, jsonpbMarshaler, jsonpbUnmarshaler, nil, nil, nil, nil, nil, runtime) + apiServer := server.StartApiServer(logger, logger, db, jsonpbMarshaler, jsonpbUnmarshaler, config, nil, nil, nil, nil, nil, nil, nil, pipeline, runtime) defer apiServer.Stop() payload := "\"Hello World\"" @@ -392,257 +364,267 @@ nakama.register_rpc(test.printWorld, "helloworld") request.Header.Add("Content-Type", "Application/JSON") res, err := client.Do(request) if err != nil { - t.Error(err) + t.Fatal(err) } b, err := ioutil.ReadAll(res.Body) if err != nil { - t.Error(err) - return + t.Fatal(err) } if string(b) != "{\"payload\":"+payload+"}" { - t.Error("Invocation failed. Return result not expected: ", string(b)) + t.Fatal("Invocation failed. Return result not expected: ", string(b)) } } func TestRuntimeHTTPRequest(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - writeLuaModule(modules, "test", ` + modules := map[string]string{ + "test": ` local nakama = require("nakama") function test(ctx, payload) local success, code, headers, body = pcall(nakama.http_request, "http://httpbin.org/status/200", "GET", {}) return tostring(code) end -nakama.register_rpc(test, "test") - `) +nakama.register_rpc(test, "test")`, + } - rp := vm(t, modules) - r := rp.Get() - defer r.Stop() + runtime, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } - fn := r.GetCallback(server.ExecutionModeRPC, "test") - m, err, _ := r.InvokeFunction(server.ExecutionModeRPC, fn, nil, "", "", 0, "", "", "", "") + fn := runtime.Rpc("test") + if fn == nil { + t.Fatal("Expected RPC function to be registered") + } + + result, err, _ := fn(nil, "", "", 0, "", "", "", "") if err != nil { - t.Error(err) + t.Fatal(err) } - if m != "200" { - t.Error("Invocation failed. Return result not expected", m) + if result != "200" { + t.Fatal("Invocation failed. Return result not expected", result) } } func TestRuntimeJson(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - writeLuaModule(modules, "test", ` + modules := map[string]string{ + "test": ` local nakama = require("nakama") function test(ctx, payload) return nakama.json_encode(nakama.json_decode(payload)) end -nakama.register_rpc(test, "test") - `) +nakama.register_rpc(test, "test")`, + } - rp := vm(t, modules) - r := rp.Get() - defer r.Stop() + runtime, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } + + fn := runtime.Rpc("test") + if fn == nil { + t.Fatal("Expected RPC function to be registered") + } payload := "{\"key\":\"value\"}" - fn := r.GetCallback(server.ExecutionModeRPC, "test") - m, err, _ := r.InvokeFunction(server.ExecutionModeRPC, fn, nil, "", "", 0, "", "", "", payload) + result, err, _ := fn(nil, "", "", 0, "", "", "", payload) if err != nil { - t.Error(err) + t.Fatal(err) } - if m != payload { - t.Error("Invocation failed. Return result not expected", m) + if result != payload { + t.Fatal("Invocation failed. Return result not expected", result) } } func TestRuntimeBase64(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - writeLuaModule(modules, "test", ` + modules := map[string]string{ + "test": ` local nakama = require("nakama") function test(ctx, payload) return nakama.base64_decode(nakama.base64_encode(payload)) end -nakama.register_rpc(test, "test") - `) +nakama.register_rpc(test, "test")`, + } + + runtime, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } - rp := vm(t, modules) - r := rp.Get() - defer r.Stop() + fn := runtime.Rpc("test") + if fn == nil { + t.Fatal("Expected RPC function to be registered") + } payload := "{\"key\":\"value\"}" - fn := r.GetCallback(server.ExecutionModeRPC, "test") - m, err, _ := r.InvokeFunction(server.ExecutionModeRPC, fn, nil, "", "", 0, "", "", "", payload) + result, err, _ := fn(nil, "", "", 0, "", "", "", payload) if err != nil { - t.Error(err) + t.Fatal(err) } - if m != payload { - t.Error("Invocation failed. Return result not expected", m) + if result != payload { + t.Fatal("Invocation failed. Return result not expected", result) } } func TestRuntimeBase16(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - writeLuaModule(modules, "test", ` + modules := map[string]string{ + "test": ` local nakama = require("nakama") function test(ctx, payload) return nakama.base16_decode(nakama.base16_encode(payload)) end -nakama.register_rpc(test, "test") - `) +nakama.register_rpc(test, "test")`, + } - rp := vm(t, modules) - r := rp.Get() - defer r.Stop() + runtime, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } + + fn := runtime.Rpc("test") + if fn == nil { + t.Fatal("Expected RPC function to be registered") + } payload := "{\"key\":\"value\"}" - fn := r.GetCallback(server.ExecutionModeRPC, "test") - m, err, _ := r.InvokeFunction(server.ExecutionModeRPC, fn, nil, "", "", 0, "", "", "", payload) + result, err, _ := fn(nil, "", "", 0, "", "", "", payload) if err != nil { - t.Error(err) + t.Fatal(err) } - if m != payload { - t.Error("Invocation failed. Return result not expected", m) + if result != payload { + t.Fatal("Invocation failed. Return result not expected", result) } } func TestRuntimeAes128(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - writeLuaModule(modules, "test", ` + modules := map[string]string{ + "test": ` local nakama = require("nakama") function test(ctx, payload) return nakama.aes128_decrypt(nakama.aes128_encrypt(payload, "goldenbridge_key"), "goldenbridge_key") end -nakama.register_rpc(test, "test") - `) +nakama.register_rpc(test, "test")`, + } - rp := vm(t, modules) - r := rp.Get() - defer r.Stop() + runtime, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } + + fn := runtime.Rpc("test") + if fn == nil { + t.Fatal("Expected RPC function to be registered") + } payload := "{\"key\":\"value\"}" - fn := r.GetCallback(server.ExecutionModeRPC, "test") - m, err, _ := r.InvokeFunction(server.ExecutionModeRPC, fn, nil, "", "", 0, "", "", "", payload) + result, err, _ := fn(nil, "", "", 0, "", "", "", payload) if err != nil { - t.Error(err) + t.Fatal(err) } - if strings.TrimSpace(m.(string)) != payload { - t.Error("Invocation failed. Return result not expected", m) + if strings.TrimSpace(result) != payload { + t.Fatal("Invocation failed. Return result not expected", result) } } func TestRuntimeMD5Hash(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - writeLuaModule(modules, "md5hash-test", ` + modules := map[string]string{ + "md5hash-test": ` local nk = require("nakama") -assert(nk.md5_hash("test") == "098f6bcd4621d373cade4e832627b4f6") -`) +assert(nk.md5_hash("test") == "098f6bcd4621d373cade4e832627b4f6")`, + } - vm(t, modules) + _, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } } func TestRuntimeSHA256Hash(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - writeLuaModule(modules, "sha256hash-test", ` + modules := map[string]string{ + "sha256hash-test": ` local nk = require("nakama") -assert(nk.sha256_hash("test") == "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08") -`) +assert(nk.sha256_hash("test") == "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08")`, + } - vm(t, modules) + _, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } } func TestRuntimeBcryptHash(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - writeLuaModule(modules, "test", ` + modules := map[string]string{ + "test": ` local nakama = require("nakama") function test(ctx, payload) return nakama.bcrypt_hash(payload) end -nakama.register_rpc(test, "test") - `) +nakama.register_rpc(test, "test")`, + } - rp := vm(t, modules) - r := rp.Get() - defer r.Stop() + runtime, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } + + fn := runtime.Rpc("test") + if fn == nil { + t.Fatal("Expected RPC function to be registered") + } payload := "{\"key\":\"value\"}" - fn := r.GetCallback(server.ExecutionModeRPC, "test") - m, err, _ := r.InvokeFunction(server.ExecutionModeRPC, fn, nil, "", "", 0, "", "", "", payload) + result, err, _ := fn(nil, "", "", 0, "", "", "", payload) if err != nil { - t.Error(err) + t.Fatal(err) } - err = bcrypt.CompareHashAndPassword([]byte(m.(string)), []byte(payload)) + err = bcrypt.CompareHashAndPassword([]byte(result), []byte(payload)) if err != nil { - t.Error("Return result not expected", m, err) + t.Fatal("Return result not expected", result, err) } } func TestRuntimeBcryptCompare(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - writeLuaModule(modules, "test", ` + modules := map[string]string{ + "test": ` local nakama = require("nakama") function test(ctx, payload) return tostring(nakama.bcrypt_compare(payload, "something_to_encrypt")) end -nakama.register_rpc(test, "test") - `) +nakama.register_rpc(test, "test")`, + } + + runtime, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } - rp := vm(t, modules) - r := rp.Get() - defer r.Stop() + fn := runtime.Rpc("test") + if fn == nil { + t.Fatal("Expected RPC function to be registered") + } payload := "something_to_encrypt" hash, _ := bcrypt.GenerateFromPassword([]byte(payload), bcrypt.DefaultCost) - fn := r.GetCallback(server.ExecutionModeRPC, "test") - m, err, _ := r.InvokeFunction(server.ExecutionModeRPC, fn, nil, "", "", 0, "", "", "", string(hash)) + result, err, _ := fn(nil, "", "", 0, "", "", "", string(hash)) if err != nil { - t.Error(err) + t.Fatal(err) } - if m != "true" { - t.Error("Return result not expected", m) + if result != "true" { + t.Error("Return result not expected", result) } } func TestRuntimeNotificationsSend(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - writeLuaModule(modules, "test", ` + modules := map[string]string{ + "test": ` local nk = require("nakama") local subject = "You've unlocked level 100!" @@ -655,20 +637,18 @@ local code = 1 local new_notifications = { { subject = subject, content = content, user_id = user_id, code = code, persistent = false} } -nk.notifications_send(new_notifications) -`) +nk.notifications_send(new_notifications)`, + } - rp := vm(t, modules) - r := rp.Get() - defer r.Stop() + _, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } } func TestRuntimeNotificationSend(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - writeLuaModule(modules, "test", ` + modules := map[string]string{ + "test": ` local nk = require("nakama") local subject = "You've unlocked level 100!" @@ -678,25 +658,22 @@ local content = { local user_id = "4c2ae592-b2a7-445e-98ec-697694478b1c" -- who to send local code = 1 -nk.notification_send(user_id, subject, content, code, "", false) -`) +nk.notification_send(user_id, subject, content, code, "", false)`, + } - rp := vm(t, modules) - r := rp.Get() - defer r.Stop() + _, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } } func TestRuntimeWalletWrite(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - db := NewDB(t) uid := uuid.FromStringOrNil("95f05d94-cc66-445a-b4d1-9e262662cf79") InsertUser(t, db, uid) - writeLuaModule(modules, "test", ` + modules := map[string]string{ + "test": ` local nk = require("nakama") local content = { @@ -704,20 +681,18 @@ local content = { } local user_id = "95f05d94-cc66-445a-b4d1-9e262662cf79" -- who to send -nk.wallet_update(user_id, content) -`) +nk.wallet_update(user_id, content)`, + } - rp := vm(t, modules) - r := rp.Get() - defer r.Stop() + _, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } } func TestRuntimeStorageWrite(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - writeLuaModule(modules, "test.lua", ` + modules := map[string]string{ + "test": ` local nk = require("nakama") local new_objects = { @@ -726,20 +701,18 @@ local new_objects = { {collection = "settings", key = "c", user_id = nil, value = {}} } -nk.storage_write(new_objects) -`) +nk.storage_write(new_objects)`, + } - rp := vm(t, modules) - r := rp.Get() - defer r.Stop() + _, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } } func TestRuntimeStorageRead(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - writeLuaModule(modules, "test.lua", ` + modules := map[string]string{ + "test": ` local nk = require("nakama") local object_ids = { {collection = "settings", key = "a", user_id = nil}, @@ -750,30 +723,31 @@ local objects = nk.storage_read(object_ids) for i, r in ipairs(objects) do assert(#r.value == 0, "'r.value' must be '{}'") -end -`) +end`, + } - rp := vm(t, modules) - r := rp.Get() - defer r.Stop() + _, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } } func TestRuntimeReqBeforeHook(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - writeLuaModule(modules, "test", ` + modules := map[string]string{ + "test": ` local nakama = require("nakama") function before_storage_write(ctx, payload) return payload end -nakama.register_req_before(before_storage_write, "WriteStorageObjects") - `) +nakama.register_req_before(before_storage_write, "WriteStorageObjects")`, + } - rp := vm(t, modules) + runtime, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } - apiserver, _ := NewAPIServer(t, rp) + apiserver, _ := NewAPIServer(t, runtime) defer apiserver.Stop() conn, client, _, ctx := NewAuthenticatedAPIClient(t, uuid.Must(uuid.NewV4()).String()) defer conn.Close() @@ -799,26 +773,26 @@ nakama.register_req_before(before_storage_write, "WriteStorageObjects") } func TestRuntimeReqBeforeHookDisallowed(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - writeLuaModule(modules, "test", ` + modules := map[string]string{ + "test": ` local nakama = require("nakama") function before_storage_write(ctx, payload) return nil end -nakama.register_req_before(before_storage_write, "WriteStorageObjects") - `) +nakama.register_req_before(before_storage_write, "WriteStorageObjects")`, + } - rp := vm(t, modules) + runtime, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } - apiserver, _ := NewAPIServer(t, rp) + apiserver, _ := NewAPIServer(t, runtime) defer apiserver.Stop() conn, client, _, ctx := NewAuthenticatedAPIClient(t, uuid.Must(uuid.NewV4()).String()) defer conn.Close() - _, err := client.WriteStorageObjects(ctx, &api.WriteStorageObjectsRequest{ + _, err = client.WriteStorageObjects(ctx, &api.WriteStorageObjectsRequest{ Objects: []*api.WriteStorageObject{{ Collection: "collection", Key: "key", @@ -835,22 +809,22 @@ nakama.register_req_before(before_storage_write, "WriteStorageObjects") } func TestRuntimeReqAfterHook(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - writeLuaModule(modules, "test", ` + modules := map[string]string{ + "test": ` local nakama = require("nakama") function after_storage_write(ctx, payload) nakama.wallet_update(ctx.user_id, {gem = 10}) return payload end -nakama.register_req_after(after_storage_write, "WriteStorageObjects") - `) +nakama.register_req_after(after_storage_write, "WriteStorageObjects")`, + } - rp := vm(t, modules) + runtime, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } - apiserver, _ := NewAPIServer(t, rp) + apiserver, _ := NewAPIServer(t, runtime) defer apiserver.Stop() conn, client, _, ctx := NewAuthenticatedAPIClient(t, uuid.Must(uuid.NewV4()).String()) defer conn.Close() @@ -871,7 +845,7 @@ nakama.register_req_after(after_storage_write, "WriteStorageObjects") } if len(acks.Acks) != 1 { - t.Error("Invocation failed. Return result not expected: ", len(acks.Acks)) + t.Fatal("Invocation failed. Return result not expected: ", len(acks.Acks)) } account, err := client.GetAccount(ctx, &empty.Empty{}) @@ -885,22 +859,22 @@ nakama.register_req_after(after_storage_write, "WriteStorageObjects") } func TestRuntimeRTBeforeHook(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - writeLuaModule(modules, "test", ` + modules := map[string]string{ + "test": ` local nakama = require("nakama") function before_match_create(ctx, payload) nakama.wallet_update(ctx.user_id, {gem = 20}) return payload end -nakama.register_rt_before(before_match_create, "MatchCreate") - `) +nakama.register_rt_before(before_match_create, "MatchCreate")`, + } - rp := vm(t, modules) + runtime, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } - apiserver, pipeline := NewAPIServer(t, rp) + apiserver, pipeline := NewAPIServer(t, runtime) defer apiserver.Stop() conn, client, s, ctx := NewAuthenticatedAPIClient(t, uuid.Must(uuid.NewV4()).String()) defer conn.Close() @@ -934,11 +908,8 @@ nakama.register_rt_before(before_match_create, "MatchCreate") } func TestRuntimeRTBeforeHookDisallow(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - writeLuaModule(modules, "test", ` + modules := map[string]string{ + "test": ` local nakama = require("nakama") function before_match_create(ctx, payload) return nil @@ -949,12 +920,15 @@ function after_match_create(ctx, payload) nakama.wallet_update(ctx.user_id, {gem = 30}) return payload end -nakama.register_rt_after(after_match_create, "MatchCreate") - `) +nakama.register_rt_after(after_match_create, "MatchCreate")`, + } - rp := vm(t, modules) + runtime, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } - apiserver, pipeline := NewAPIServer(t, rp) + apiserver, pipeline := NewAPIServer(t, runtime) defer apiserver.Stop() conn, client, s, ctx := NewAuthenticatedAPIClient(t, uuid.Must(uuid.NewV4()).String()) defer conn.Close() @@ -988,11 +962,8 @@ nakama.register_rt_after(after_match_create, "MatchCreate") } func TestRuntimeGroupTests(t *testing.T) { - modules := &server.ModuleCache{ - Names: make([]string, 0), - Modules: make(map[string]*server.RuntimeModule, 0), - } - writeLuaModule(modules, "test.lua", ` + modules := map[string]string{ + "test": ` local nk = require("nakama") local user_id = nk.uuid_v4() @@ -1020,8 +991,11 @@ do assert(g.state == 1, "'g.state' must be equal to 1 / superadmin") end -nk.group_delete(group.id) -`) +nk.group_delete(group.id)`, + } - vm(t, modules) + _, err := runtimeWithModules(t, modules) + if err != nil { + t.Fatal(err.Error()) + } } diff --git a/tests/util.go b/tests/util.go index 1b850ccdb0062a797c33b0e5675c2878061ffefa..d289c076477847ee2fe879e24812b951e06425eb 100644 --- a/tests/util.go +++ b/tests/util.go @@ -20,12 +20,6 @@ import ( "database/sql" "encoding/base64" "encoding/json" - "os" - "strconv" - "strings" - "testing" - "time" - "github.com/gofrs/uuid" "github.com/golang/protobuf/jsonpb" "github.com/heroiclabs/nakama/api" @@ -35,6 +29,9 @@ import ( "go.uber.org/zap/zapcore" "google.golang.org/grpc" "google.golang.org/grpc/metadata" + "os" + "strings" + "testing" ) var ( @@ -83,6 +80,12 @@ func (d *DummySession) Consume(func(logger *zap.Logger, session server.Session, func (d *DummySession) Format() server.SessionFormat { return server.SessionFormatJson } +func (d *DummySession) ClientIP() string { + return "" +} +func (d *DummySession) ClientPort() string { + return "" +} func (d *DummySession) Send(isStream bool, mode uint8, envelope *rtapi.Envelope) error { d.messages = append(d.messages, envelope) return nil @@ -135,7 +138,7 @@ func NewDB(t *testing.T) *sql.DB { } func GenerateString() string { - return strconv.FormatInt(time.Now().UTC().UnixNano(), 10) + return uuid.Must(uuid.NewV4()).String() } func InsertUser(t *testing.T, db *sql.DB, uid uuid.UUID) { @@ -147,12 +150,12 @@ ON CONFLICT(id) DO NOTHING`, uid, uid.String()); err != nil { } } -func NewAPIServer(t *testing.T, runtimePool *server.RuntimePool) (*server.ApiServer, *server.Pipeline) { +func NewAPIServer(t *testing.T, runtime *server.Runtime) (*server.ApiServer, *server.Pipeline) { db := NewDB(t) router := &DummyMessageRouter{} tracker := &server.LocalTracker{} - pipeline := server.NewPipeline(config, db, jsonpbMarshaler, jsonpbUnmarshaler, nil, nil, nil, tracker, router, runtimePool) - apiServer := server.StartApiServer(logger, logger, db, jsonpbMarshaler, jsonpbUnmarshaler, config, nil, nil, nil, nil, nil, tracker, router, pipeline, runtimePool) + pipeline := server.NewPipeline(logger, config, db, jsonpbMarshaler, jsonpbUnmarshaler, nil, nil, nil, tracker, router, runtime) + apiServer := server.StartApiServer(logger, logger, db, jsonpbMarshaler, jsonpbUnmarshaler, config, nil, nil, nil, nil, nil, tracker, router, pipeline, runtime) return apiServer, pipeline }