diff --git a/CHANGELOG.md b/CHANGELOG.md index 738529027964fe9bf39fd5ea8ffdf9b90a378e79..6cd2107f4a1fcfdedd9900f1504304487641dd10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [keep a changelog](http://keepachangelog.com) and this pr - Expose session fields in authoritative match join attempt contexts. - Add group ID to content of in-app notifications relating to groups. - New runtime function to get a single match by ID. +- New runtime functions for link operations. ### Changed - Replace metrics implementation. @@ -19,6 +20,7 @@ The format is based on [keep a changelog](http://keepachangelog.com) and this pr - Developer console status snapshot gauges more accurately reflect current server metrics. - Disconnect match participants when an authoritative match ends due to an error. - Build with Go 1.14.3 release. +- Update to nakama-common 1.5.0. ### Fixed - Ensure runtime environment values do not appear multiple times in the devconsole configuration view. @@ -26,6 +28,7 @@ The format is based on [keep a changelog](http://keepachangelog.com) and this pr - Developer console status graphs correctly show a fixed time window of metrics. - Expose friend deletion in developer console user detail view. - Expose group membership deletion in developer console user detail view. +- Password is no longer expected when unlinking emails. ## [2.11.1] - 2020-03-29 ### Changed diff --git a/go.mod b/go.mod index cdfd6c1f8018f4778e7535313d35fd00001c0f29..b0d1e696b9a86f49971d852f2bbd88109f4b0f43 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/gorilla/mux v1.7.4 github.com/gorilla/websocket v1.4.2 github.com/grpc-ecosystem/grpc-gateway v1.13.0 - github.com/heroiclabs/nakama-common v1.4.0 + github.com/heroiclabs/nakama-common v1.5.0 github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect github.com/jackc/pgx v3.5.0+incompatible github.com/jmhodges/levigo v1.0.0 // indirect diff --git a/go.sum b/go.sum index 09e4efb4f3815fd22cf85b66f3257020c0b138cc..7c25ed517928b882e6cfb921253454dae13066f2 100644 --- a/go.sum +++ b/go.sum @@ -80,8 +80,6 @@ github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -102,8 +100,8 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/grpc-ecosystem/grpc-gateway v1.13.0 h1:sBDQoHXrOlfPobnKw69FIKa1wg9qsLLvvQ/Y19WtFgI= github.com/grpc-ecosystem/grpc-gateway v1.13.0/go.mod h1:8XEsbTttt/W+VvjtQhLACqCisSPWTxCZ7sBRjU6iH9c= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/heroiclabs/nakama-common v1.4.0 h1:7IRb+FIYKa4iOY08lTVmZ5kHHNaDNWEQE9+w4bWNdCU= -github.com/heroiclabs/nakama-common v1.4.0/go.mod h1:0lw1qUs5FhJlox1WsEPdODRsdvfZ7Q1c/2jxkbeL++M= +github.com/heroiclabs/nakama-common v1.5.0 h1:AdTdt/sYvbi5vf3Q0y2YJQhCxAW1mZpCQPbjr26OUIg= +github.com/heroiclabs/nakama-common v1.5.0/go.mod h1:nZAXHdeo4SyPlCyf7pU9rCVizxEhBF74gt7teDe/EaQ= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= diff --git a/server/api_link.go b/server/api_link.go index 987c39ed69b1dfb0be986159ba8f20b8f8259452..1501c30ff2c5942e72c753e0f50977c184e4a857 100644 --- a/server/api_link.go +++ b/server/api_link.go @@ -16,32 +16,27 @@ package server import ( "context" - "strconv" - "strings" - "github.com/gofrs/uuid" "github.com/golang/protobuf/ptypes/empty" "github.com/heroiclabs/nakama-common/api" - "github.com/jackc/pgx" "go.uber.org/zap" - "golang.org/x/crypto/bcrypt" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) func (s *ApiServer) LinkCustom(ctx context.Context, in *api.AccountCustom) (*empty.Empty, error) { - userID := ctx.Value(ctxUserIDKey{}) + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) // Before hook. if fn := s.runtime.BeforeLinkCustom(); fn != nil { beforeFn := func(clientIP, clientPort string) error { - result, err, code := fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + result, err, code := fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) if err != nil { return 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", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.(uuid.UUID).String())) + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.String())) return status.Error(codes.NotFound, "Requested resource was not found.") } in = result @@ -55,37 +50,15 @@ func (s *ApiServer) LinkCustom(ctx context.Context, in *api.AccountCustom) (*emp } } - customID := in.Id - if customID == "" { - return nil, status.Error(codes.InvalidArgument, "Custom ID is required.") - } else if invalidCharsRegex.MatchString(customID) { - return nil, status.Error(codes.InvalidArgument, "Invalid custom ID, no spaces or control characters allowed.") - } else if len(customID) < 6 || len(customID) > 128 { - return nil, status.Error(codes.InvalidArgument, "Invalid custom ID, must be 6-128 bytes.") - } - - res, err := s.db.ExecContext(ctx, ` -UPDATE users -SET custom_id = $2, update_time = now() -WHERE (id = $1) -AND (NOT EXISTS - (SELECT id - FROM users - WHERE custom_id = $2 AND NOT id = $1))`, - userID, - customID) - + err := LinkCustom(ctx, s.logger, s.db, userID, in.Id) if err != nil { - s.logger.Error("Could not link custom ID.", zap.Error(err), zap.Any("input", in)) - return nil, status.Error(codes.Internal, "Error while trying to link Custom ID.") - } else if count, _ := res.RowsAffected(); count == 0 { - return nil, status.Error(codes.AlreadyExists, "Custom ID is already in use.") + return nil, err } // After hook. if fn := s.runtime.AfterLinkCustom(); fn != nil { afterFn := func(clientIP, clientPort string) error { - return fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + return fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) } // Execute the after function lambda wrapped in a trace for stats measurement. @@ -96,18 +69,18 @@ AND (NOT EXISTS } func (s *ApiServer) LinkDevice(ctx context.Context, in *api.AccountDevice) (*empty.Empty, error) { - userID := ctx.Value(ctxUserIDKey{}) + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) // Before hook. if fn := s.runtime.BeforeLinkDevice(); fn != nil { beforeFn := func(clientIP, clientPort string) error { - result, err, code := fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + result, err, code := fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) if err != nil { return 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", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.(uuid.UUID).String())) + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.String())) return status.Error(codes.NotFound, "Requested resource was not found.") } in = result @@ -121,60 +94,15 @@ func (s *ApiServer) LinkDevice(ctx context.Context, in *api.AccountDevice) (*emp } } - deviceID := in.Id - if deviceID == "" { - return nil, status.Error(codes.InvalidArgument, "Device ID is required.") - } else if invalidCharsRegex.MatchString(deviceID) { - return nil, status.Error(codes.InvalidArgument, "Device ID invalid, no spaces or control characters allowed.") - } else if len(deviceID) < 10 || len(deviceID) > 128 { - return nil, status.Error(codes.InvalidArgument, "Device ID invalid, must be 10-128 bytes.") - } - - tx, err := s.db.BeginTx(ctx, nil) - if err != nil { - s.logger.Error("Could not begin database transaction.", zap.Error(err)) - return nil, status.Error(codes.Internal, "Error linking Device ID.") - } - - err = ExecuteInTx(ctx, tx, func() error { - var dbDeviceIDLinkedUser int64 - err := tx.QueryRowContext(ctx, "SELECT COUNT(id) FROM user_device WHERE id = $1 AND user_id = $2 LIMIT 1", deviceID, userID).Scan(&dbDeviceIDLinkedUser) - if err != nil { - s.logger.Debug("Cannot link device ID.", zap.Error(err), zap.Any("input", in)) - return err - } - - if dbDeviceIDLinkedUser == 0 { - _, err = tx.ExecContext(ctx, "INSERT INTO user_device (id, user_id) VALUES ($1, $2)", deviceID, userID) - if err != nil { - if e, ok := err.(pgx.PgError); ok && e.Code == dbErrorUniqueViolation { - return StatusError(codes.AlreadyExists, "Device ID already in use.", err) - } - s.logger.Debug("Cannot link device ID.", zap.Error(err), zap.Any("input", in)) - return err - } - } - - _, err = tx.ExecContext(ctx, "UPDATE users SET update_time = now() WHERE id = $1", userID) - if err != nil { - s.logger.Debug("Cannot update users table while linking.", zap.Error(err), zap.Any("input", in)) - return err - } - return nil - }) - + err := LinkDevice(ctx, s.logger, s.db, userID, in.Id) if err != nil { - if e, ok := err.(*statusError); ok { - return nil, e.Status() - } - s.logger.Error("Error in database transaction.", zap.Error(err)) - return nil, status.Error(codes.Internal, "Error linking Device ID.") + return nil, err } // After hook. if fn := s.runtime.AfterLinkDevice(); fn != nil { afterFn := func(clientIP, clientPort string) error { - return fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + return fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) } // Execute the after function lambda wrapped in a trace for stats measurement. @@ -185,18 +113,18 @@ func (s *ApiServer) LinkDevice(ctx context.Context, in *api.AccountDevice) (*emp } func (s *ApiServer) LinkEmail(ctx context.Context, in *api.AccountEmail) (*empty.Empty, error) { - userID := ctx.Value(ctxUserIDKey{}) + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) // Before hook. if fn := s.runtime.BeforeLinkEmail(); fn != nil { beforeFn := func(clientIP, clientPort string) error { - result, err, code := fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + result, err, code := fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) if err != nil { return 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", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.(uuid.UUID).String())) + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.String())) return status.Error(codes.NotFound, "Requested resource was not found.") } in = result @@ -210,44 +138,15 @@ func (s *ApiServer) LinkEmail(ctx context.Context, in *api.AccountEmail) (*empty } } - if in.Email == "" || in.Password == "" { - return nil, status.Error(codes.InvalidArgument, "Email address and password is required.") - } else if invalidCharsRegex.MatchString(in.Email) { - return nil, status.Error(codes.InvalidArgument, "Invalid email address, no spaces or control characters allowed.") - } else if len(in.Password) < 8 { - return nil, status.Error(codes.InvalidArgument, "Password must be at least 8 characters long.") - } else if !emailRegex.MatchString(in.Email) { - return nil, status.Error(codes.InvalidArgument, "Invalid email address format.") - } else if len(in.Email) < 10 || len(in.Email) > 255 { - return nil, status.Error(codes.InvalidArgument, "Invalid email address, must be 10-255 bytes.") - } - - cleanEmail := strings.ToLower(in.Email) - hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost) - - res, err := s.db.ExecContext(ctx, ` -UPDATE users -SET email = $2, password = $3, update_time = now() -WHERE (id = $1) -AND (NOT EXISTS - (SELECT id - FROM users - WHERE email = $2 AND NOT id = $1))`, - userID, - cleanEmail, - hashedPassword) - + err := LinkEmail(ctx, s.logger, s.db, userID, in.Email, in.Password) if err != nil { - s.logger.Error("Could not link email.", zap.Error(err), zap.Any("input", in)) - return nil, status.Error(codes.Internal, "Error while trying to link email.") - } else if count, _ := res.RowsAffected(); count == 0 { - return nil, status.Error(codes.AlreadyExists, "Email is already in use.") + return nil, err } // After hook. if fn := s.runtime.AfterLinkEmail(); fn != nil { afterFn := func(clientIP, clientPort string) error { - return fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + return fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) } // Execute the after function lambda wrapped in a trace for stats measurement. @@ -258,18 +157,19 @@ AND (NOT EXISTS } func (s *ApiServer) LinkFacebook(ctx context.Context, in *api.LinkFacebookRequest) (*empty.Empty, error) { - userID := ctx.Value(ctxUserIDKey{}) + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) + username := ctx.Value(ctxUsernameKey{}).(string) // Before hook. if fn := s.runtime.BeforeLinkFacebook(); fn != nil { beforeFn := func(clientIP, clientPort string) error { - result, err, code := fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + result, err, code := fn(ctx, s.logger, userID.String(), username, ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) if err != nil { return 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", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.(uuid.UUID).String())) + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.String())) return status.Error(codes.NotFound, "Requested resource was not found.") } in = result @@ -287,39 +187,15 @@ func (s *ApiServer) LinkFacebook(ctx context.Context, in *api.LinkFacebookReques return nil, status.Error(codes.InvalidArgument, "Facebook access token is required.") } - facebookProfile, err := s.socialClient.GetFacebookProfile(ctx, in.Account.Token) + err := LinkFacebook(ctx, s.logger, s.db, s.socialClient, s.router, userID, username, in.Account.Token, in.Sync == nil || in.Sync.Value) if err != nil { - s.logger.Info("Could not authenticate Facebook profile.", zap.Error(err)) - return nil, status.Error(codes.Unauthenticated, "Could not authenticate Facebook profile.") - } - - res, err := s.db.ExecContext(ctx, ` -UPDATE users -SET facebook_id = $2, update_time = now() -WHERE (id = $1) -AND (NOT EXISTS - (SELECT id - FROM users - WHERE facebook_id = $2 AND NOT id = $1))`, - userID, - facebookProfile.ID) - - if err != nil { - s.logger.Error("Could not link Facebook ID.", zap.Error(err), zap.Any("input", in)) - return nil, status.Error(codes.Internal, "Error while trying to link Facebook ID.") - } else if count, _ := res.RowsAffected(); count == 0 { - return nil, status.Error(codes.AlreadyExists, "Facebook ID is already in use.") - } - - // Import friends if requested. - if in.Sync == nil || in.Sync.Value { - _ = importFacebookFriends(ctx, s.logger, s.db, s.router, s.socialClient, userID.(uuid.UUID), ctx.Value(ctxUsernameKey{}).(string), in.Account.Token, false) + return nil, err } // After hook. if fn := s.runtime.AfterLinkFacebook(); fn != nil { afterFn := func(clientIP, clientPort string) error { - return fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + return fn(ctx, s.logger, userID.String(), username, ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) } // Execute the after function lambda wrapped in a trace for stats measurement. @@ -330,18 +206,18 @@ AND (NOT EXISTS } func (s *ApiServer) LinkFacebookInstantGame(ctx context.Context, in *api.AccountFacebookInstantGame) (*empty.Empty, error) { - userID := ctx.Value(ctxUserIDKey{}) + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) // Before hook. if fn := s.runtime.BeforeLinkFacebookInstantGame(); fn != nil { beforeFn := func(clientIP, clientPort string) error { - result, err, code := fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + result, err, code := fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) if err != nil { return 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", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.(uuid.UUID).String())) + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.String())) return status.Error(codes.NotFound, "Requested resource was not found.") } in = result @@ -359,34 +235,15 @@ func (s *ApiServer) LinkFacebookInstantGame(ctx context.Context, in *api.Account return nil, status.Error(codes.InvalidArgument, "Signed Player Info for a Facebook Instant Game is required.") } - facebookInstantGameID, err := s.socialClient.ExtractFacebookInstantGameID(in.SignedPlayerInfo, s.config.GetSocial().FacebookInstantGame.AppSecret) + err := LinkFacebookInstantGame(ctx, s.logger, s.db, s.config, s.socialClient, userID, in.SignedPlayerInfo) if err != nil { - s.logger.Info("Could not authenticate Facebook Instant Game profile.", zap.Error(err)) - return nil, status.Error(codes.Unauthenticated, "Could not authenticate Facebook Instant Game profile.") - } - - res, err := s.db.ExecContext(ctx, ` -UPDATE users -SET facebook_instant_game_id = $2, update_time = now() -WHERE (id = $1) -AND (NOT EXISTS - (SELECT id - FROM users - WHERE facebook_instant_game_id = $2 AND NOT id = $1))`, - userID, - facebookInstantGameID) - - if err != nil { - s.logger.Error("Could not link Facebook Instant Game ID.", zap.Error(err), zap.Any("input", in)) - return nil, status.Error(codes.Internal, "Error while trying to link Facebook Instant Game ID.") - } else if count, _ := res.RowsAffected(); count == 0 { - return nil, status.Error(codes.AlreadyExists, "Facebook Instant Game ID is already in use.") + return nil, err } // After hook. if fn := s.runtime.AfterLinkFacebookInstantGame(); fn != nil { afterFn := func(clientIP, clientPort string) error { - return fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + return fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) } // Execute the after function lambda wrapped in a trace for stats measurement. @@ -397,18 +254,18 @@ AND (NOT EXISTS } func (s *ApiServer) LinkGameCenter(ctx context.Context, in *api.AccountGameCenter) (*empty.Empty, error) { - userID := ctx.Value(ctxUserIDKey{}) + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) // Before hook. if fn := s.runtime.BeforeLinkGameCenter(); fn != nil { beforeFn := func(clientIP, clientPort string) error { - result, err, code := fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + result, err, code := fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) if err != nil { return 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", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.(uuid.UUID).String())) + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.String())) return status.Error(codes.NotFound, "Requested resource was not found.") } in = result @@ -422,48 +279,15 @@ func (s *ApiServer) LinkGameCenter(ctx context.Context, in *api.AccountGameCente } } - if in.BundleId == "" { - return nil, status.Error(codes.InvalidArgument, "GameCenter bundle ID is required.") - } else if in.PlayerId == "" { - return nil, status.Error(codes.InvalidArgument, "GameCenter player ID is required.") - } else if in.PublicKeyUrl == "" { - return nil, status.Error(codes.InvalidArgument, "GameCenter public key URL is required.") - } else if in.Salt == "" { - return nil, status.Error(codes.InvalidArgument, "GameCenter salt is required.") - } else if in.Signature == "" { - return nil, status.Error(codes.InvalidArgument, "GameCenter signature is required.") - } else if in.TimestampSeconds == 0 { - return nil, status.Error(codes.InvalidArgument, "GameCenter timestamp is required.") - } - - valid, err := s.socialClient.CheckGameCenterID(ctx, in.PlayerId, in.BundleId, in.TimestampSeconds, in.Salt, in.Signature, in.PublicKeyUrl) - if !valid || err != nil { - s.logger.Info("Could not authenticate GameCenter profile.", zap.Error(err), zap.Bool("valid", valid)) - return nil, status.Error(codes.Unauthenticated, "Could not authenticate GameCenter profile.") - } - - res, err := s.db.ExecContext(ctx, ` -UPDATE users -SET gamecenter_id = $2, update_time = now() -WHERE (id = $1) -AND (NOT EXISTS - (SELECT id - FROM users - WHERE gamecenter_id = $2 AND NOT id = $1))`, - userID, - in.PlayerId) - + err := LinkGameCenter(ctx, s.logger, s.db, s.socialClient, userID, in.PlayerId, in.BundleId, in.TimestampSeconds, in.Salt, in.Signature, in.PublicKeyUrl) if err != nil { - s.logger.Error("Could not link GameCenter ID.", zap.Error(err), zap.Any("input", in)) - return nil, status.Error(codes.Internal, "Error while trying to link GameCenter ID.") - } else if count, _ := res.RowsAffected(); count == 0 { - return nil, status.Error(codes.AlreadyExists, "GameCenter ID is already in use.") + return nil, err } // After hook. if fn := s.runtime.AfterLinkGameCenter(); fn != nil { afterFn := func(clientIP, clientPort string) error { - return fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + return fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) } // Execute the after function lambda wrapped in a trace for stats measurement. @@ -474,18 +298,18 @@ AND (NOT EXISTS } func (s *ApiServer) LinkGoogle(ctx context.Context, in *api.AccountGoogle) (*empty.Empty, error) { - userID := ctx.Value(ctxUserIDKey{}) + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) // Before hook. if fn := s.runtime.BeforeLinkGoogle(); fn != nil { beforeFn := func(clientIP, clientPort string) error { - result, err, code := fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + result, err, code := fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) if err != nil { return 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", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.(uuid.UUID).String())) + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.String())) return status.Error(codes.NotFound, "Requested resource was not found.") } in = result @@ -499,52 +323,15 @@ func (s *ApiServer) LinkGoogle(ctx context.Context, in *api.AccountGoogle) (*emp } } - if in.Token == "" { - return nil, status.Error(codes.InvalidArgument, "Google access token is required.") - } - - googleProfile, err := s.socialClient.CheckGoogleToken(ctx, in.Token) - if err != nil { - s.logger.Info("Could not authenticate Google profile.", zap.Error(err)) - return nil, status.Error(codes.Unauthenticated, "Could not authenticate Google profile.") - } - - displayName := googleProfile.Name - if len(displayName) > 255 { - // Ignore the name in case it is longer than db can store - s.logger.Warn("Skipping updating display_name: value received from Google longer than max length of 255 chars.", zap.String("display_name", displayName)) - displayName = "" - } - - avatarURL := googleProfile.Picture - if len(avatarURL) > 512 { - // Ignore the url in case it is longer than db can store - s.logger.Warn("Skipping updating avatar_url: value received from Google longer than max length of 512 chars.", zap.String("avatar_url", avatarURL)) - avatarURL = "" - } - - res, err := s.db.ExecContext(ctx, ` -UPDATE users -SET google_id = $2, display_name = $3, avatar_url = $4, update_time = now() -WHERE (id = $1) -AND (NOT EXISTS - (SELECT id - FROM users - WHERE google_id = $2 AND NOT id = $1))`, - userID, - googleProfile.Sub, displayName, avatarURL) - + err := LinkGoogle(ctx, s.logger, s.db, s.socialClient, userID, in.Token) if err != nil { - s.logger.Error("Could not link Google ID.", zap.Error(err), zap.Any("input", in)) - return nil, status.Error(codes.Internal, "Error while trying to link Google ID.") - } else if count, _ := res.RowsAffected(); count == 0 { - return nil, status.Error(codes.AlreadyExists, "Google ID is already in use.") + return nil, err } // After hook. if fn := s.runtime.AfterLinkGoogle(); fn != nil { afterFn := func(clientIP, clientPort string) error { - return fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + return fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) } // Execute the after function lambda wrapped in a trace for stats measurement. @@ -555,18 +342,18 @@ AND (NOT EXISTS } func (s *ApiServer) LinkSteam(ctx context.Context, in *api.AccountSteam) (*empty.Empty, error) { - userID := ctx.Value(ctxUserIDKey{}) + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) // Before hook. if fn := s.runtime.BeforeLinkSteam(); fn != nil { beforeFn := func(clientIP, clientPort string) error { - result, err, code := fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + result, err, code := fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) if err != nil { return 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", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.(uuid.UUID).String())) + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.String())) return status.Error(codes.NotFound, "Requested resource was not found.") } in = result @@ -580,42 +367,15 @@ func (s *ApiServer) LinkSteam(ctx context.Context, in *api.AccountSteam) (*empty } } - if s.config.GetSocial().Steam.PublisherKey == "" || s.config.GetSocial().Steam.AppID == 0 { - return nil, status.Error(codes.FailedPrecondition, "Steam authentication is not configured.") - } - - if in.Token == "" { - return nil, status.Error(codes.InvalidArgument, "Steam access token is required.") - } - - steamProfile, err := s.socialClient.GetSteamProfile(ctx, s.config.GetSocial().Steam.PublisherKey, s.config.GetSocial().Steam.AppID, in.Token) - if err != nil { - s.logger.Info("Could not authenticate Steam profile.", zap.Error(err)) - return nil, status.Error(codes.Unauthenticated, "Could not authenticate Steam profile.") - } - - res, err := s.db.ExecContext(ctx, ` -UPDATE users -SET steam_id = $2, update_time = now() -WHERE (id = $1) -AND (NOT EXISTS - (SELECT id - FROM users - WHERE steam_id = $2 AND NOT id = $1))`, - userID, - strconv.FormatUint(steamProfile.SteamID, 10)) - + err := LinkSteam(ctx, s.logger, s.db, s.config, s.socialClient, userID, in.Token) if err != nil { - s.logger.Error("Could not link Steam ID.", zap.Error(err), zap.Any("input", in)) - return nil, status.Error(codes.Internal, "Error while trying to link Steam ID.") - } else if count, _ := res.RowsAffected(); count == 0 { - return nil, status.Error(codes.AlreadyExists, "Steam ID is already in use.") + return nil, err } // After hook. if fn := s.runtime.AfterLinkSteam(); fn != nil { afterFn := func(clientIP, clientPort string) error { - return fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + return fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) } // Execute the after function lambda wrapped in a trace for stats measurement. diff --git a/server/api_unlink.go b/server/api_unlink.go index 448f7019dcf67ec01775567d0226401563fa5d3d..b3ff5f08088e25cf3608f9620e2c35a9a9187cd9 100644 --- a/server/api_unlink.go +++ b/server/api_unlink.go @@ -16,9 +16,6 @@ package server import ( "context" - "strconv" - "strings" - "github.com/gofrs/uuid" "github.com/golang/protobuf/ptypes/empty" "github.com/heroiclabs/nakama-common/api" @@ -28,18 +25,18 @@ import ( ) func (s *ApiServer) UnlinkCustom(ctx context.Context, in *api.AccountCustom) (*empty.Empty, error) { - userID := ctx.Value(ctxUserIDKey{}) + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) // Before hook. if fn := s.runtime.BeforeUnlinkCustom(); fn != nil { beforeFn := func(clientIP, clientPort string) error { - result, err, code := fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + result, err, code := fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) if err != nil { return 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", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.(uuid.UUID).String())) + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.String())) return status.Error(codes.NotFound, "Requested resource was not found.") } in = result @@ -53,35 +50,15 @@ func (s *ApiServer) UnlinkCustom(ctx context.Context, in *api.AccountCustom) (*e } } - if in.GetId() == "" { - return nil, status.Error(codes.InvalidArgument, "An ID must be supplied.") - } - - query := `UPDATE users SET custom_id = NULL, update_time = now() -WHERE id = $1 -AND custom_id = $2 -AND ((facebook_id IS NOT NULL - OR facebook_instant_game_id IS NOT NULL - OR google_id IS NOT NULL - OR gamecenter_id IS NOT NULL - OR steam_id IS NOT NULL - OR email IS NOT NULL) - OR - EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))` - - res, err := s.db.ExecContext(ctx, query, userID, in.Id) - + err := UnlinkCustom(ctx, s.logger, s.db, userID, in.Id) if err != nil { - s.logger.Error("Could not unlink custom ID.", zap.Error(err), zap.Any("input", in)) - return nil, status.Error(codes.Internal, "Error while trying to unlink custom ID.") - } else if count, _ := res.RowsAffected(); count == 0 { - return nil, status.Error(codes.PermissionDenied, "Cannot unlink last account identifier. Check profile exists and is not last link.") + return nil, err } // After hook. if fn := s.runtime.AfterUnlinkCustom(); fn != nil { afterFn := func(clientIP, clientPort string) error { - return fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + return fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) } // Execute the after function lambda wrapped in a trace for stats measurement. @@ -92,18 +69,18 @@ AND ((facebook_id IS NOT NULL } func (s *ApiServer) UnlinkDevice(ctx context.Context, in *api.AccountDevice) (*empty.Empty, error) { - userID := ctx.Value(ctxUserIDKey{}) + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) // Before hook. if fn := s.runtime.BeforeUnlinkDevice(); fn != nil { beforeFn := func(clientIP, clientPort string) error { - result, err, code := fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + result, err, code := fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) if err != nil { return 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", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.(uuid.UUID).String())) + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.String())) return status.Error(codes.NotFound, "Requested resource was not found.") } in = result @@ -117,61 +94,15 @@ func (s *ApiServer) UnlinkDevice(ctx context.Context, in *api.AccountDevice) (*e } } - if in.GetId() == "" { - return nil, status.Error(codes.InvalidArgument, "A device ID must be supplied.") - } - - tx, err := s.db.BeginTx(ctx, nil) - if err != nil { - s.logger.Error("Could not begin database transaction.", zap.Error(err)) - return nil, status.Error(codes.Internal, "Could not unlink Device ID.") - } - - err = ExecuteInTx(ctx, tx, func() error { - 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 - OR facebook_instant_game_id IS NOT NULL - OR google_id IS NOT NULL - OR gamecenter_id IS NOT NULL - OR steam_id IS NOT NULL - OR email IS NOT NULL - OR custom_id IS NOT NULL)) - OR EXISTS (SELECT id FROM user_device WHERE user_id = $1 AND id <> $2 LIMIT 1))` - - res, err := tx.ExecContext(ctx, query, userID, in.Id) - if err != nil { - s.logger.Debug("Could not unlink device ID.", zap.Error(err), zap.Any("input", in)) - return err - } - if count, _ := res.RowsAffected(); count == 0 { - return StatusError(codes.PermissionDenied, "Cannot unlink last account identifier. Check profile exists and is not last link.", ErrRowsAffectedCount) - } - - res, err = tx.ExecContext(ctx, "UPDATE users SET update_time = now() WHERE id = $1", userID) - if err != nil { - s.logger.Debug("Could not unlink device ID.", zap.Error(err), zap.Any("input", in)) - return err - } - if count, _ := res.RowsAffected(); count == 0 { - return StatusError(codes.PermissionDenied, "Cannot unlink last account identifier. Check profile exists and is not last link.", ErrRowsAffectedCount) - } - - return nil - }) - + err := UnlinkDevice(ctx, s.logger, s.db, userID, in.Id) if err != nil { - if e, ok := err.(*statusError); ok { - return nil, e.Status() - } - s.logger.Error("Error in database transaction.", zap.Error(err)) - return nil, status.Error(codes.Internal, "Could not unlink device ID.") + return nil, err } // After hook. if fn := s.runtime.AfterUnlinkDevice(); fn != nil { afterFn := func(clientIP, clientPort string) error { - return fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + return fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) } // Execute the after function lambda wrapped in a trace for stats measurement. @@ -182,18 +113,18 @@ AND (EXISTS (SELECT id FROM users WHERE id = $1 AND } func (s *ApiServer) UnlinkEmail(ctx context.Context, in *api.AccountEmail) (*empty.Empty, error) { - userID := ctx.Value(ctxUserIDKey{}) + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) // Before hook. if fn := s.runtime.BeforeUnlinkEmail(); fn != nil { beforeFn := func(clientIP, clientPort string) error { - result, err, code := fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + result, err, code := fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) if err != nil { return 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", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.(uuid.UUID).String())) + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.String())) return status.Error(codes.NotFound, "Requested resource was not found.") } in = result @@ -207,36 +138,15 @@ func (s *ApiServer) UnlinkEmail(ctx context.Context, in *api.AccountEmail) (*emp } } - if in.GetEmail() == "" || in.GetPassword() == "" { - return nil, status.Error(codes.InvalidArgument, "Both email and password must be supplied.") - } - - query := `UPDATE users SET email = NULL, password = NULL, update_time = now() -WHERE id = $1 -AND email = $2 -AND ((facebook_id IS NOT NULL - OR facebook_instant_game_id IS NOT NULL - OR google_id IS NOT NULL - OR gamecenter_id IS NOT NULL - OR steam_id IS NOT NULL - OR custom_id IS NOT NULL) - OR - EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))` - - cleanEmail := strings.ToLower(in.Email) - res, err := s.db.ExecContext(ctx, query, userID, cleanEmail) - + err := UnlinkEmail(ctx, s.logger, s.db, userID, in.Email) if err != nil { - s.logger.Error("Could not unlink email.", zap.Error(err), zap.Any("input", in)) - return nil, status.Error(codes.Internal, "Error while trying to unlink email.") - } else if count, _ := res.RowsAffected(); count == 0 { - return nil, status.Error(codes.PermissionDenied, "Cannot unlink last account identifier. Check profile exists and is not last link.") + return nil, err } // After hook. if fn := s.runtime.AfterUnlinkEmail(); fn != nil { afterFn := func(clientIP, clientPort string) error { - return fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + return fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) } // Execute the after function lambda wrapped in a trace for stats measurement. @@ -247,18 +157,18 @@ AND ((facebook_id IS NOT NULL } func (s *ApiServer) UnlinkFacebook(ctx context.Context, in *api.AccountFacebook) (*empty.Empty, error) { - userID := ctx.Value(ctxUserIDKey{}) + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) // Before hook. if fn := s.runtime.BeforeUnlinkFacebook(); fn != nil { beforeFn := func(clientIP, clientPort string) error { - result, err, code := fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + result, err, code := fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) if err != nil { return 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", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.(uuid.UUID).String())) + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.String())) return status.Error(codes.NotFound, "Requested resource was not found.") } in = result @@ -272,40 +182,15 @@ func (s *ApiServer) UnlinkFacebook(ctx context.Context, in *api.AccountFacebook) } } - if in.Token == "" { - return nil, status.Error(codes.InvalidArgument, "Facebook access token is required.") - } - - facebookProfile, err := s.socialClient.GetFacebookProfile(ctx, in.Token) - if err != nil { - s.logger.Info("Could not authenticate Facebook profile.", zap.Error(err)) - return nil, status.Error(codes.Unauthenticated, "Could not authenticate Facebook profile.") - } - - query := `UPDATE users SET facebook_id = NULL, update_time = now() -WHERE id = $1 -AND facebook_id = $2 -AND ((custom_id IS NOT NULL - OR google_id IS NOT NULL - OR gamecenter_id IS NOT NULL - OR steam_id IS NOT NULL - OR email IS NOT NULL) - OR - EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))` - - res, err := s.db.ExecContext(ctx, query, userID, facebookProfile.ID) - + err := UnlinkFacebook(ctx, s.logger, s.db, s.socialClient, userID, in.Token) if err != nil { - s.logger.Error("Could not unlink Facebook ID.", zap.Error(err), zap.Any("input", in)) - return nil, status.Error(codes.Internal, "Error while trying to unlink Facebook ID.") - } else if count, _ := res.RowsAffected(); count == 0 { - return nil, status.Error(codes.PermissionDenied, "Cannot unlink last account identifier. Check profile exists and is not last link.") + return nil, err } // After hook. if fn := s.runtime.AfterUnlinkFacebook(); fn != nil { afterFn := func(clientIP, clientPort string) error { - return fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + return fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) } // Execute the after function lambda wrapped in a trace for stats measurement. @@ -316,18 +201,18 @@ AND ((custom_id IS NOT NULL } func (s *ApiServer) UnlinkFacebookInstantGame(ctx context.Context, in *api.AccountFacebookInstantGame) (*empty.Empty, error) { - userID := ctx.Value(ctxUserIDKey{}) + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) // Before hook. if fn := s.runtime.BeforeUnlinkFacebookInstantGame(); fn != nil { beforeFn := func(clientIP, clientPort string) error { - result, err, code := fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + result, err, code := fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) if err != nil { return 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", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.(uuid.UUID).String())) + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.String())) return status.Error(codes.NotFound, "Requested resource was not found.") } in = result @@ -341,41 +226,15 @@ func (s *ApiServer) UnlinkFacebookInstantGame(ctx context.Context, in *api.Accou } } - if in.SignedPlayerInfo == "" { - return nil, status.Error(codes.InvalidArgument, "Signed Player Info for a Facebook Instant Game is required.") - } - - facebookInstantGameID, err := s.socialClient.ExtractFacebookInstantGameID(in.SignedPlayerInfo, s.config.GetSocial().FacebookInstantGame.AppSecret) - if err != nil { - s.logger.Info("Could not authenticate Facebook Instant Game profile.", zap.Error(err)) - return nil, status.Error(codes.Unauthenticated, "Could not authenticate Facebook Instant Game profile.") - } - - query := `UPDATE users SET facebook_instant_game_id = NULL, update_time = now() -WHERE id = $1 -AND facebook_instant_game_id = $2 -AND ((custom_id IS NOT NULL - OR google_id IS NOT NULL - OR facebook_id IS NOT NULL - OR gamecenter_id IS NOT NULL - OR steam_id IS NOT NULL - OR email IS NOT NULL) - OR - EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))` - - res, err := s.db.ExecContext(ctx, query, userID, facebookInstantGameID) - + err := UnlinkFacebookInstantGame(ctx, s.logger, s.db, s.config, s.socialClient, userID, in.SignedPlayerInfo) if err != nil { - s.logger.Error("Could not unlink Facebook Instant Game ID.", zap.Error(err), zap.Any("input", in)) - return nil, status.Error(codes.Internal, "Error while trying to unlink Facebook Instant Game ID.") - } else if count, _ := res.RowsAffected(); count == 0 { - return nil, status.Error(codes.PermissionDenied, "Cannot unlink last account identifier. Check profile exists and is not last link.") + return nil, err } // After hook. if fn := s.runtime.AfterUnlinkFacebookInstantGame(); fn != nil { afterFn := func(clientIP, clientPort string) error { - return fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + return fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) } // Execute the after function lambda wrapped in a trace for stats measurement. @@ -386,18 +245,18 @@ AND ((custom_id IS NOT NULL } func (s *ApiServer) UnlinkGameCenter(ctx context.Context, in *api.AccountGameCenter) (*empty.Empty, error) { - userID := ctx.Value(ctxUserIDKey{}) + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) // Before hook. if fn := s.runtime.BeforeUnlinkGameCenter(); fn != nil { beforeFn := func(clientIP, clientPort string) error { - result, err, code := fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + result, err, code := fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) if err != nil { return 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", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.(uuid.UUID).String())) + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.String())) return status.Error(codes.NotFound, "Requested resource was not found.") } in = result @@ -411,51 +270,15 @@ func (s *ApiServer) UnlinkGameCenter(ctx context.Context, in *api.AccountGameCen } } - if in.BundleId == "" { - return nil, status.Error(codes.InvalidArgument, "GameCenter bundle ID is required.") - } else if in.PlayerId == "" { - return nil, status.Error(codes.InvalidArgument, "GameCenter player ID is required.") - } else if in.PublicKeyUrl == "" { - return nil, status.Error(codes.InvalidArgument, "GameCenter public key URL is required.") - } else if in.Salt == "" { - return nil, status.Error(codes.InvalidArgument, "GameCenter salt is required.") - } else if in.Signature == "" { - return nil, status.Error(codes.InvalidArgument, "GameCenter signature is required.") - } else if in.TimestampSeconds == 0 { - return nil, status.Error(codes.InvalidArgument, "GameCenter timestamp is required.") - } - - valid, err := s.socialClient.CheckGameCenterID(ctx, in.PlayerId, in.BundleId, in.TimestampSeconds, in.Salt, in.Signature, in.PublicKeyUrl) - if !valid || err != nil { - s.logger.Info("Could not authenticate GameCenter profile.", zap.Error(err), zap.Bool("valid", valid)) - return nil, status.Error(codes.Unauthenticated, "Could not authenticate GameCenter profile.") - } - - query := `UPDATE users SET gamecenter_id = NULL, update_time = now() -WHERE id = $1 -AND gamecenter_id = $2 -AND ((custom_id IS NOT NULL - OR google_id IS NOT NULL - OR facebook_id IS NOT NULL - OR facebook_instant_game_id IS NOT NULL - OR steam_id IS NOT NULL - OR email IS NOT NULL) - OR - EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))` - - res, err := s.db.ExecContext(ctx, query, userID, in.PlayerId) - + err := UnlinkGameCenter(ctx, s.logger, s.db, s.socialClient, userID, in.PlayerId, in.BundleId, in.TimestampSeconds, in.Salt, in.Signature, in.PublicKeyUrl) if err != nil { - s.logger.Error("Could not unlink GameCenter ID.", zap.Error(err), zap.Any("input", in)) - return nil, status.Error(codes.Internal, "Error while trying to unlink GameCenter ID.") - } else if count, _ := res.RowsAffected(); count == 0 { - return nil, status.Error(codes.PermissionDenied, "Cannot unlink last account identifier. Check profile exists and is not last link.") + return nil, err } // After hook. if fn := s.runtime.AfterUnlinkGameCenter(); fn != nil { afterFn := func(clientIP, clientPort string) error { - return fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + return fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) } // Execute the after function lambda wrapped in a trace for stats measurement. @@ -466,18 +289,18 @@ AND ((custom_id IS NOT NULL } func (s *ApiServer) UnlinkGoogle(ctx context.Context, in *api.AccountGoogle) (*empty.Empty, error) { - userID := ctx.Value(ctxUserIDKey{}) + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) // Before hook. if fn := s.runtime.BeforeUnlinkGoogle(); fn != nil { beforeFn := func(clientIP, clientPort string) error { - result, err, code := fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + result, err, code := fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) if err != nil { return 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", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.(uuid.UUID).String())) + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.String())) return status.Error(codes.NotFound, "Requested resource was not found.") } in = result @@ -491,41 +314,15 @@ func (s *ApiServer) UnlinkGoogle(ctx context.Context, in *api.AccountGoogle) (*e } } - if in.Token == "" { - return nil, status.Error(codes.InvalidArgument, "Google access token is required.") - } - - googleProfile, err := s.socialClient.CheckGoogleToken(ctx, in.Token) + err := UnlinkGoogle(ctx, s.logger, s.db, s.socialClient, userID, in.Token) if err != nil { - s.logger.Info("Could not authenticate Google profile.", zap.Error(err)) - return nil, status.Error(codes.Unauthenticated, "Could not authenticate Google profile.") - } - - query := `UPDATE users SET google_id = NULL, update_time = now() -WHERE id = $1 -AND google_id = $2 -AND ((custom_id IS NOT NULL - OR gamecenter_id IS NOT NULL - OR facebook_id IS NOT NULL - OR facebook_instant_game_id IS NOT NULL - OR steam_id IS NOT NULL - OR email IS NOT NULL) - OR - EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))` - - res, err := s.db.ExecContext(ctx, query, userID, googleProfile.Sub) - - if err != nil { - s.logger.Error("Could not unlink Google ID.", zap.Error(err), zap.Any("input", in)) - return nil, status.Error(codes.Internal, "Error while trying to unlink Google ID.") - } else if count, _ := res.RowsAffected(); count == 0 { - return nil, status.Error(codes.PermissionDenied, "Cannot unlink last account identifier. Check profile exists and is not last link.") + return nil, err } // After hook. if fn := s.runtime.AfterUnlinkGoogle(); fn != nil { afterFn := func(clientIP, clientPort string) error { - return fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + return fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) } // Execute the after function lambda wrapped in a trace for stats measurement. @@ -536,18 +333,18 @@ AND ((custom_id IS NOT NULL } func (s *ApiServer) UnlinkSteam(ctx context.Context, in *api.AccountSteam) (*empty.Empty, error) { - userID := ctx.Value(ctxUserIDKey{}) + userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) // Before hook. if fn := s.runtime.BeforeUnlinkSteam(); fn != nil { beforeFn := func(clientIP, clientPort string) error { - result, err, code := fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + result, err, code := fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) if err != nil { return 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", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.(uuid.UUID).String())) + s.logger.Warn("Intercepted a disabled resource.", zap.Any("resource", ctx.Value(ctxFullMethodKey{}).(string)), zap.String("uid", userID.String())) return status.Error(codes.NotFound, "Requested resource was not found.") } in = result @@ -561,45 +358,15 @@ func (s *ApiServer) UnlinkSteam(ctx context.Context, in *api.AccountSteam) (*emp } } - if s.config.GetSocial().Steam.PublisherKey == "" || s.config.GetSocial().Steam.AppID == 0 { - return nil, status.Error(codes.FailedPrecondition, "Steam authentication is not configured.") - } - - if in.Token == "" { - return nil, status.Error(codes.InvalidArgument, "Steam access token is required.") - } - - steamProfile, err := s.socialClient.GetSteamProfile(ctx, s.config.GetSocial().Steam.PublisherKey, s.config.GetSocial().Steam.AppID, in.Token) - if err != nil { - s.logger.Info("Could not authenticate Steam profile.", zap.Error(err)) - return nil, status.Error(codes.Unauthenticated, "Could not authenticate Steam profile.") - } - - query := `UPDATE users SET steam_id = NULL, update_time = now() -WHERE id = $1 -AND steam_id = $2 -AND ((custom_id IS NOT NULL - OR gamecenter_id IS NOT NULL - OR facebook_id IS NOT NULL - OR facebook_instant_game_id IS NOT NULL - OR google_id IS NOT NULL - OR email IS NOT NULL) - OR - EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))` - - res, err := s.db.ExecContext(ctx, query, userID, strconv.FormatUint(steamProfile.SteamID, 10)) - + err := UnlinkSteam(ctx, s.logger, s.db, s.config, s.socialClient, userID, in.Token) if err != nil { - s.logger.Error("Could not unlink Steam ID.", zap.Error(err), zap.Any("input", in)) - return nil, status.Error(codes.Internal, "Error while trying to unlink Steam ID.") - } else if count, _ := res.RowsAffected(); count == 0 { - return nil, status.Error(codes.PermissionDenied, "Cannot unlink last account identifier. Check profile exists and is not last link.") + return nil, err } // After hook. if fn := s.runtime.AfterUnlinkSteam(); fn != nil { afterFn := func(clientIP, clientPort string) error { - return fn(ctx, s.logger, userID.(uuid.UUID).String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) + return fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxVarsKey{}).(map[string]string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) } // Execute the after function lambda wrapped in a trace for stats measurement. diff --git a/server/core_link.go b/server/core_link.go new file mode 100644 index 0000000000000000000000000000000000000000..6e3a553b9a8f583186935a3aa41e5feb19d54c1d --- /dev/null +++ b/server/core_link.go @@ -0,0 +1,336 @@ +// Copyright 2020 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" + "github.com/gofrs/uuid" + "github.com/heroiclabs/nakama/v2/social" + "github.com/jackc/pgx" + "go.uber.org/zap" + "golang.org/x/crypto/bcrypt" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "strconv" + "strings" +) + +func LinkCustom(ctx context.Context, logger *zap.Logger, db *sql.DB, userID uuid.UUID, customID string) error { + if customID == "" { + return status.Error(codes.InvalidArgument, "Custom ID is required.") + } else if invalidCharsRegex.MatchString(customID) { + return status.Error(codes.InvalidArgument, "Invalid custom ID, no spaces or control characters allowed.") + } else if len(customID) < 6 || len(customID) > 128 { + return status.Error(codes.InvalidArgument, "Invalid custom ID, must be 6-128 bytes.") + } + + res, err := db.ExecContext(ctx, ` +UPDATE users +SET custom_id = $2, update_time = now() +WHERE (id = $1) +AND (NOT EXISTS + (SELECT id + FROM users + WHERE custom_id = $2 AND NOT id = $1))`, + userID, + customID) + + if err != nil { + logger.Error("Could not link custom ID.", zap.Error(err), zap.Any("input", customID)) + return status.Error(codes.Internal, "Error while trying to link Custom ID.") + } else if count, _ := res.RowsAffected(); count == 0 { + return status.Error(codes.AlreadyExists, "Custom ID is already in use.") + } + return nil +} + +func LinkDevice(ctx context.Context, logger *zap.Logger, db *sql.DB, userID uuid.UUID, deviceID string) error { + if deviceID == "" { + return status.Error(codes.InvalidArgument, "Device ID is required.") + } else if invalidCharsRegex.MatchString(deviceID) { + return status.Error(codes.InvalidArgument, "Device ID invalid, no spaces or control characters allowed.") + } else if len(deviceID) < 10 || len(deviceID) > 128 { + return status.Error(codes.InvalidArgument, "Device ID invalid, must be 10-128 bytes.") + } + + tx, err := db.BeginTx(ctx, nil) + if err != nil { + logger.Error("Could not begin database transaction.", zap.Error(err)) + return status.Error(codes.Internal, "Error linking Device ID.") + } + + err = ExecuteInTx(ctx, tx, func() error { + var dbDeviceIDLinkedUser int64 + err := tx.QueryRowContext(ctx, "SELECT COUNT(id) FROM user_device WHERE id = $1 AND user_id = $2 LIMIT 1", deviceID, userID).Scan(&dbDeviceIDLinkedUser) + if err != nil { + logger.Debug("Cannot link device ID.", zap.Error(err), zap.Any("input", deviceID)) + return err + } + + if dbDeviceIDLinkedUser == 0 { + _, err = tx.ExecContext(ctx, "INSERT INTO user_device (id, user_id) VALUES ($1, $2)", deviceID, userID) + if err != nil { + if e, ok := err.(pgx.PgError); ok && e.Code == dbErrorUniqueViolation { + return StatusError(codes.AlreadyExists, "Device ID already in use.", err) + } + logger.Debug("Cannot link device ID.", zap.Error(err), zap.Any("input", deviceID)) + return err + } + } + + _, err = tx.ExecContext(ctx, "UPDATE users SET update_time = now() WHERE id = $1", userID) + if err != nil { + logger.Debug("Cannot update users table while linking.", zap.Error(err), zap.Any("input", deviceID)) + return err + } + return nil + }) + + if err != nil { + if e, ok := err.(*statusError); ok { + return e.Status() + } + logger.Error("Error in database transaction.", zap.Error(err)) + return status.Error(codes.Internal, "Error linking Device ID.") + } + return nil +} + +func LinkEmail(ctx context.Context, logger *zap.Logger, db *sql.DB, userID uuid.UUID, email, password string) error { + if email == "" || password == "" { + return status.Error(codes.InvalidArgument, "Email address and password is required.") + } else if invalidCharsRegex.MatchString(email) { + return status.Error(codes.InvalidArgument, "Invalid email address, no spaces or control characters allowed.") + } else if len(password) < 8 { + return status.Error(codes.InvalidArgument, "Password must be at least 8 characters long.") + } else if !emailRegex.MatchString(email) { + return status.Error(codes.InvalidArgument, "Invalid email address format.") + } else if len(email) < 10 || len(email) > 255 { + return status.Error(codes.InvalidArgument, "Invalid email address, must be 10-255 bytes.") + } + + cleanEmail := strings.ToLower(email) + hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + + res, err := db.ExecContext(ctx, ` +UPDATE users +SET email = $2, password = $3, update_time = now() +WHERE (id = $1) +AND (NOT EXISTS + (SELECT id + FROM users + WHERE email = $2 AND NOT id = $1))`, + userID, + cleanEmail, + hashedPassword) + + if err != nil { + logger.Error("Could not link email.", zap.Error(err), zap.Any("input", email)) + return status.Error(codes.Internal, "Error while trying to link email.") + } else if count, _ := res.RowsAffected(); count == 0 { + return status.Error(codes.AlreadyExists, "Email is already in use.") + } + return nil +} + +func LinkFacebook(ctx context.Context, logger *zap.Logger, db *sql.DB, socialClient *social.Client, router MessageRouter, userID uuid.UUID, username, token string, sync bool) error { + if token == "" { + return status.Error(codes.InvalidArgument, "Facebook access token is required.") + } + + facebookProfile, err := socialClient.GetFacebookProfile(ctx, token) + if err != nil { + logger.Info("Could not authenticate Facebook profile.", zap.Error(err)) + return status.Error(codes.Unauthenticated, "Could not authenticate Facebook profile.") + } + + res, err := db.ExecContext(ctx, ` +UPDATE users +SET facebook_id = $2, update_time = now() +WHERE (id = $1) +AND (NOT EXISTS + (SELECT id + FROM users + WHERE facebook_id = $2 AND NOT id = $1))`, + userID, + facebookProfile.ID) + + if err != nil { + logger.Error("Could not link Facebook ID.", zap.Error(err), zap.Any("input", token)) + return status.Error(codes.Internal, "Error while trying to link Facebook ID.") + } else if count, _ := res.RowsAffected(); count == 0 { + return status.Error(codes.AlreadyExists, "Facebook ID is already in use.") + } + + // Import friends if requested. + if sync { + _ = importFacebookFriends(ctx, logger, db, router, socialClient, userID, username, token, false) + } + + return nil +} + +func LinkFacebookInstantGame(ctx context.Context, logger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, userID uuid.UUID, signedPlayerInfo string) error { + if signedPlayerInfo == "" { + return status.Error(codes.InvalidArgument, "Signed Player Info for a Facebook Instant Game is required.") + } + + facebookInstantGameID, err := socialClient.ExtractFacebookInstantGameID(signedPlayerInfo, config.GetSocial().FacebookInstantGame.AppSecret) + if err != nil { + logger.Info("Could not authenticate Facebook Instant Game profile.", zap.Error(err)) + return status.Error(codes.Unauthenticated, "Could not authenticate Facebook Instant Game profile.") + } + + res, err := db.ExecContext(ctx, ` +UPDATE users +SET facebook_instant_game_id = $2, update_time = now() +WHERE (id = $1) +AND (NOT EXISTS + (SELECT id + FROM users + WHERE facebook_instant_game_id = $2 AND NOT id = $1))`, + userID, + facebookInstantGameID) + + if err != nil { + logger.Error("Could not link Facebook Instant Game ID.", zap.Error(err), zap.Any("input", signedPlayerInfo)) + return status.Error(codes.Internal, "Error while trying to link Facebook Instant Game ID.") + } else if count, _ := res.RowsAffected(); count == 0 { + return status.Error(codes.AlreadyExists, "Facebook Instant Game ID is already in use.") + } + return nil +} + +func LinkGameCenter(ctx context.Context, logger *zap.Logger, db *sql.DB, socialClient *social.Client, userID uuid.UUID, playerID string, bundleID string, timestamp int64, salt string, signature string, publicKeyURL string) error { + if bundleID == "" { + return status.Error(codes.InvalidArgument, "GameCenter bundle ID is required.") + } else if playerID == "" { + return status.Error(codes.InvalidArgument, "GameCenter player ID is required.") + } else if publicKeyURL == "" { + return status.Error(codes.InvalidArgument, "GameCenter public key URL is required.") + } else if salt == "" { + return status.Error(codes.InvalidArgument, "GameCenter salt is required.") + } else if signature == "" { + return status.Error(codes.InvalidArgument, "GameCenter signature is required.") + } else if timestamp == 0 { + return status.Error(codes.InvalidArgument, "GameCenter timestamp is required.") + } + + valid, err := socialClient.CheckGameCenterID(ctx, playerID, bundleID, timestamp, salt, signature, publicKeyURL) + if !valid || err != nil { + logger.Info("Could not authenticate GameCenter profile.", zap.Error(err), zap.Bool("valid", valid)) + return status.Error(codes.Unauthenticated, "Could not authenticate GameCenter profile.") + } + + res, err := db.ExecContext(ctx, ` +UPDATE users +SET gamecenter_id = $2, update_time = now() +WHERE (id = $1) +AND (NOT EXISTS + (SELECT id + FROM users + WHERE gamecenter_id = $2 AND NOT id = $1))`, + userID, + playerID) + + if err != nil { + logger.Error("Could not link GameCenter ID.", zap.Error(err), zap.Any("input", playerID)) + return status.Error(codes.Internal, "Error while trying to link GameCenter ID.") + } else if count, _ := res.RowsAffected(); count == 0 { + return status.Error(codes.AlreadyExists, "GameCenter ID is already in use.") + } + return nil +} + +func LinkGoogle(ctx context.Context, logger *zap.Logger, db *sql.DB, socialClient *social.Client, userID uuid.UUID, token string) error { + if token == "" { + return status.Error(codes.InvalidArgument, "Google access token is required.") + } + + googleProfile, err := socialClient.CheckGoogleToken(ctx, token) + if err != nil { + logger.Info("Could not authenticate Google profile.", zap.Error(err)) + return status.Error(codes.Unauthenticated, "Could not authenticate Google profile.") + } + + displayName := googleProfile.Name + if len(displayName) > 255 { + // Ignore the name in case it is longer than db can store + logger.Warn("Skipping updating display_name: value received from Google longer than max length of 255 chars.", zap.String("display_name", displayName)) + displayName = "" + } + + avatarURL := googleProfile.Picture + if len(avatarURL) > 512 { + // Ignore the url in case it is longer than db can store + logger.Warn("Skipping updating avatar_url: value received from Google longer than max length of 512 chars.", zap.String("avatar_url", avatarURL)) + avatarURL = "" + } + + res, err := db.ExecContext(ctx, ` +UPDATE users +SET google_id = $2, display_name = $3, avatar_url = $4, update_time = now() +WHERE (id = $1) +AND (NOT EXISTS + (SELECT id + FROM users + WHERE google_id = $2 AND NOT id = $1))`, + userID, + googleProfile.Sub, displayName, avatarURL) + + if err != nil { + logger.Error("Could not link Google ID.", zap.Error(err), zap.Any("input", token)) + return status.Error(codes.Internal, "Error while trying to link Google ID.") + } else if count, _ := res.RowsAffected(); count == 0 { + return status.Error(codes.AlreadyExists, "Google ID is already in use.") + } + return nil +} + +func LinkSteam(ctx context.Context, logger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, userID uuid.UUID, token string) error { + if config.GetSocial().Steam.PublisherKey == "" || config.GetSocial().Steam.AppID == 0 { + return status.Error(codes.FailedPrecondition, "Steam authentication is not configured.") + } + + if token == "" { + return status.Error(codes.InvalidArgument, "Steam access token is required.") + } + + steamProfile, err := socialClient.GetSteamProfile(ctx, config.GetSocial().Steam.PublisherKey, config.GetSocial().Steam.AppID, token) + if err != nil { + logger.Info("Could not authenticate Steam profile.", zap.Error(err)) + return status.Error(codes.Unauthenticated, "Could not authenticate Steam profile.") + } + + res, err := db.ExecContext(ctx, ` +UPDATE users +SET steam_id = $2, update_time = now() +WHERE (id = $1) +AND (NOT EXISTS + (SELECT id + FROM users + WHERE steam_id = $2 AND NOT id = $1))`, + userID, + strconv.FormatUint(steamProfile.SteamID, 10)) + + if err != nil { + logger.Error("Could not link Steam ID.", zap.Error(err), zap.Any("input", token)) + return status.Error(codes.Internal, "Error while trying to link Steam ID.") + } else if count, _ := res.RowsAffected(); count == 0 { + return status.Error(codes.AlreadyExists, "Steam ID is already in use.") + } + return nil +} diff --git a/server/core_unlink.go b/server/core_unlink.go new file mode 100644 index 0000000000000000000000000000000000000000..a00694052afabbd8ec835a603b06a096e21ad286 --- /dev/null +++ b/server/core_unlink.go @@ -0,0 +1,305 @@ +// Copyright 2020 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" + "github.com/gofrs/uuid" + "github.com/heroiclabs/nakama/v2/social" + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "strconv" + "strings" +) + +func UnlinkCustom(ctx context.Context, logger *zap.Logger, db *sql.DB, id uuid.UUID, customID string) error { + if customID == "" { + return status.Error(codes.InvalidArgument, "An ID must be supplied.") + } + + res, err := db.ExecContext(ctx, `UPDATE users SET custom_id = NULL, update_time = now() +WHERE id = $1 +AND custom_id = $2 +AND ((facebook_id IS NOT NULL + OR facebook_instant_game_id IS NOT NULL + OR google_id IS NOT NULL + OR gamecenter_id IS NOT NULL + OR steam_id IS NOT NULL + OR email IS NOT NULL) + OR + EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))`, id, customID) + + if err != nil { + logger.Error("Could not unlink custom ID.", zap.Error(err), zap.Any("input", customID)) + return status.Error(codes.Internal, "Error while trying to unlink custom ID.") + } else if count, _ := res.RowsAffected(); count == 0 { + return status.Error(codes.PermissionDenied, "Cannot unlink last account identifier. Check profile exists and is not last link.") + } + return nil +} + +func UnlinkDevice(ctx context.Context, logger *zap.Logger, db *sql.DB, id uuid.UUID, deviceID string) error { + if deviceID == "" { + return status.Error(codes.InvalidArgument, "A device ID must be supplied.") + } + + tx, err := db.BeginTx(ctx, nil) + if err != nil { + logger.Error("Could not begin database transaction.", zap.Error(err)) + return status.Error(codes.Internal, "Could not unlink Device ID.") + } + + err = ExecuteInTx(ctx, tx, func() error { + res, err := tx.ExecContext(ctx, `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 + OR facebook_instant_game_id IS NOT NULL + OR google_id IS NOT NULL + OR gamecenter_id IS NOT NULL + OR steam_id IS NOT NULL + OR email IS NOT NULL + OR custom_id IS NOT NULL)) + OR EXISTS (SELECT id FROM user_device WHERE user_id = $1 AND id <> $2 LIMIT 1))`, id, deviceID) + if err != nil { + logger.Debug("Could not unlink device ID.", zap.Error(err), zap.Any("input", deviceID)) + return err + } + if count, _ := res.RowsAffected(); count == 0 { + return StatusError(codes.PermissionDenied, "Cannot unlink last account identifier. Check profile exists and is not last link.", ErrRowsAffectedCount) + } + + res, err = tx.ExecContext(ctx, "UPDATE users SET update_time = now() WHERE id = $1", id) + if err != nil { + logger.Debug("Could not unlink device ID.", zap.Error(err), zap.Any("input", deviceID)) + return err + } + if count, _ := res.RowsAffected(); count == 0 { + return StatusError(codes.PermissionDenied, "Cannot unlink last account identifier. Check profile exists and is not last link.", ErrRowsAffectedCount) + } + + return nil + }) + + if err != nil { + if e, ok := err.(*statusError); ok { + return e.Status() + } + logger.Error("Error in database transaction.", zap.Error(err)) + return status.Error(codes.Internal, "Could not unlink device ID.") + } + return nil +} + +func UnlinkEmail(ctx context.Context, logger *zap.Logger, db *sql.DB, id uuid.UUID, email string) error { + if email == "" { + return status.Error(codes.InvalidArgument, "Both email and password must be supplied.") + } + cleanEmail := strings.ToLower(email) + + res, err := db.ExecContext(ctx, `UPDATE users SET email = NULL, password = NULL, update_time = now() +WHERE id = $1 +AND email = $2 +AND ((facebook_id IS NOT NULL + OR facebook_instant_game_id IS NOT NULL + OR google_id IS NOT NULL + OR gamecenter_id IS NOT NULL + OR steam_id IS NOT NULL + OR custom_id IS NOT NULL) + OR + EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))`, id, cleanEmail) + + if err != nil { + logger.Error("Could not unlink email.", zap.Error(err), zap.Any("input", email)) + return status.Error(codes.Internal, "Error while trying to unlink email.") + } else if count, _ := res.RowsAffected(); count == 0 { + return status.Error(codes.PermissionDenied, "Cannot unlink last account identifier. Check profile exists and is not last link.") + } + return nil +} + +func UnlinkFacebook(ctx context.Context, logger *zap.Logger, db *sql.DB, socialClient *social.Client, id uuid.UUID, token string) error { + if token == "" { + return status.Error(codes.InvalidArgument, "Facebook access token is required.") + } + + facebookProfile, err := socialClient.GetFacebookProfile(ctx, token) + if err != nil { + logger.Info("Could not authenticate Facebook profile.", zap.Error(err)) + return status.Error(codes.Unauthenticated, "Could not authenticate Facebook profile.") + } + + res, err := db.ExecContext(ctx, `UPDATE users SET facebook_id = NULL, update_time = now() +WHERE id = $1 +AND facebook_id = $2 +AND ((custom_id IS NOT NULL + OR google_id IS NOT NULL + OR gamecenter_id IS NOT NULL + OR steam_id IS NOT NULL + OR email IS NOT NULL) + OR + EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))`, id, facebookProfile.ID) + + if err != nil { + logger.Error("Could not unlink Facebook ID.", zap.Error(err), zap.Any("input", token)) + return status.Error(codes.Internal, "Error while trying to unlink Facebook ID.") + } else if count, _ := res.RowsAffected(); count == 0 { + return status.Error(codes.PermissionDenied, "Cannot unlink last account identifier. Check profile exists and is not last link.") + } + return nil +} + +func UnlinkFacebookInstantGame(ctx context.Context, logger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, id uuid.UUID, signedPlayerInfo string) error { + if signedPlayerInfo == "" { + return status.Error(codes.InvalidArgument, "Signed Player Info for a Facebook Instant Game is required.") + } + + facebookInstantGameID, err := socialClient.ExtractFacebookInstantGameID(signedPlayerInfo, config.GetSocial().FacebookInstantGame.AppSecret) + if err != nil { + logger.Info("Could not authenticate Facebook Instant Game profile.", zap.Error(err)) + return status.Error(codes.Unauthenticated, "Could not authenticate Facebook Instant Game profile.") + } + + res, err := db.ExecContext(ctx, `UPDATE users SET facebook_instant_game_id = NULL, update_time = now() +WHERE id = $1 +AND facebook_instant_game_id = $2 +AND ((custom_id IS NOT NULL + OR google_id IS NOT NULL + OR facebook_id IS NOT NULL + OR gamecenter_id IS NOT NULL + OR steam_id IS NOT NULL + OR email IS NOT NULL) + OR + EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))`, id, facebookInstantGameID) + + if err != nil { + logger.Error("Could not unlink Facebook Instant Game ID.", zap.Error(err), zap.Any("input", signedPlayerInfo)) + return status.Error(codes.Internal, "Error while trying to unlink Facebook Instant Game ID.") + } else if count, _ := res.RowsAffected(); count == 0 { + return status.Error(codes.PermissionDenied, "Cannot unlink last account identifier. Check profile exists and is not last link.") + } + return nil +} + +func UnlinkGameCenter(ctx context.Context, logger *zap.Logger, db *sql.DB, socialClient *social.Client, id uuid.UUID, playerID string, bundleID string, timestamp int64, salt string, signature string, publicKeyURL string) error { + if bundleID == "" { + return status.Error(codes.InvalidArgument, "GameCenter bundle ID is required.") + } else if playerID == "" { + return status.Error(codes.InvalidArgument, "GameCenter player ID is required.") + } else if publicKeyURL == "" { + return status.Error(codes.InvalidArgument, "GameCenter public key URL is required.") + } else if salt == "" { + return status.Error(codes.InvalidArgument, "GameCenter salt is required.") + } else if signature == "" { + return status.Error(codes.InvalidArgument, "GameCenter signature is required.") + } else if timestamp == 0 { + return status.Error(codes.InvalidArgument, "GameCenter timestamp is required.") + } + + valid, err := socialClient.CheckGameCenterID(ctx, playerID, bundleID, timestamp, salt, signature, publicKeyURL) + if !valid || err != nil { + logger.Info("Could not authenticate GameCenter profile.", zap.Error(err), zap.Bool("valid", valid)) + return status.Error(codes.Unauthenticated, "Could not authenticate GameCenter profile.") + } + + res, err := db.ExecContext(ctx, `UPDATE users SET gamecenter_id = NULL, update_time = now() +WHERE id = $1 +AND gamecenter_id = $2 +AND ((custom_id IS NOT NULL + OR google_id IS NOT NULL + OR facebook_id IS NOT NULL + OR facebook_instant_game_id IS NOT NULL + OR steam_id IS NOT NULL + OR email IS NOT NULL) + OR + EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))`, id, playerID) + + if err != nil { + logger.Error("Could not unlink GameCenter ID.", zap.Error(err), zap.Any("input", playerID)) + return status.Error(codes.Internal, "Error while trying to unlink GameCenter ID.") + } else if count, _ := res.RowsAffected(); count == 0 { + return status.Error(codes.PermissionDenied, "Cannot unlink last account identifier. Check profile exists and is not last link.") + } + return nil +} + +func UnlinkGoogle(ctx context.Context, logger *zap.Logger, db *sql.DB, socialClient *social.Client, id uuid.UUID, token string) error { + if token == "" { + return status.Error(codes.InvalidArgument, "Google access token is required.") + } + + googleProfile, err := socialClient.CheckGoogleToken(ctx, token) + if err != nil { + logger.Info("Could not authenticate Google profile.", zap.Error(err)) + return status.Error(codes.Unauthenticated, "Could not authenticate Google profile.") + } + + res, err := db.ExecContext(ctx, `UPDATE users SET google_id = NULL, update_time = now() +WHERE id = $1 +AND google_id = $2 +AND ((custom_id IS NOT NULL + OR gamecenter_id IS NOT NULL + OR facebook_id IS NOT NULL + OR facebook_instant_game_id IS NOT NULL + OR steam_id IS NOT NULL + OR email IS NOT NULL) + OR + EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))`, id, googleProfile.Sub) + + if err != nil { + logger.Error("Could not unlink Google ID.", zap.Error(err), zap.Any("input", token)) + return status.Error(codes.Internal, "Error while trying to unlink Google ID.") + } else if count, _ := res.RowsAffected(); count == 0 { + return status.Error(codes.PermissionDenied, "Cannot unlink last account identifier. Check profile exists and is not last link.") + } + return nil +} + +func UnlinkSteam(ctx context.Context, logger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, id uuid.UUID, token string) error { + if config.GetSocial().Steam.PublisherKey == "" || config.GetSocial().Steam.AppID == 0 { + return status.Error(codes.FailedPrecondition, "Steam authentication is not configured.") + } + + if token == "" { + return status.Error(codes.InvalidArgument, "Steam access token is required.") + } + + steamProfile, err := socialClient.GetSteamProfile(ctx, config.GetSocial().Steam.PublisherKey, config.GetSocial().Steam.AppID, token) + if err != nil { + logger.Info("Could not authenticate Steam profile.", zap.Error(err)) + return status.Error(codes.Unauthenticated, "Could not authenticate Steam profile.") + } + + res, err := db.ExecContext(ctx, `UPDATE users SET steam_id = NULL, update_time = now() +WHERE id = $1 +AND steam_id = $2 +AND ((custom_id IS NOT NULL + OR gamecenter_id IS NOT NULL + OR facebook_id IS NOT NULL + OR facebook_instant_game_id IS NOT NULL + OR google_id IS NOT NULL + OR email IS NOT NULL) + OR + EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))`, id, strconv.FormatUint(steamProfile.SteamID, 10)) + + if err != nil { + logger.Error("Could not unlink Steam ID.", zap.Error(err), zap.Any("input", token)) + return status.Error(codes.Internal, "Error while trying to unlink Steam ID.") + } else if count, _ := res.RowsAffected(); count == 0 { + return status.Error(codes.PermissionDenied, "Cannot unlink last account identifier. Check profile exists and is not last link.") + } + return nil +} diff --git a/server/runtime_go_nakama.go b/server/runtime_go_nakama.go index 5ae9a07325bbdbf23ab649a8096652114d6fb22c..0a9d3cb6cb83d4c7233c945d9c2dae3b25c0a79c 100644 --- a/server/runtime_go_nakama.go +++ b/server/runtime_go_nakama.go @@ -183,7 +183,7 @@ func (n *RuntimeGoNakamaModule) AuthenticateFacebook(ctx context.Context, token return dbUserID, dbUsername, created, err } -func (n *RuntimeGoNakamaModule) AuthenticateFacebookInstantGame(ctx context.Context, appSecret string, signedPlayerInfo string, username string, create bool) (string, string, bool, error) { +func (n *RuntimeGoNakamaModule) AuthenticateFacebookInstantGame(ctx context.Context, signedPlayerInfo string, username string, create bool) (string, string, bool, error) { if signedPlayerInfo == "" { return "", "", false, errors.New("expects signed player info") } @@ -196,7 +196,7 @@ func (n *RuntimeGoNakamaModule) AuthenticateFacebookInstantGame(ctx context.Cont return "", "", false, errors.New("expects id to be valid, must be 1-128 bytes") } - return AuthenticateFacebookInstantGame(ctx, n.logger, n.db, n.socialClient, appSecret, signedPlayerInfo, username, create) + return AuthenticateFacebookInstantGame(ctx, n.logger, n.db, n.socialClient, n.config.GetSocial().FacebookInstantGame.AppSecret, signedPlayerInfo, username, create) } func (n *RuntimeGoNakamaModule) AuthenticateGameCenter(ctx context.Context, playerID, bundleID string, timestamp int64, salt, signature, publicKeyUrl, username string, create bool) (string, string, bool, error) { @@ -445,6 +445,150 @@ func (n *RuntimeGoNakamaModule) UsersUnbanId(ctx context.Context, userIDs []stri return UnbanUsers(ctx, n.logger, n.db, userIDs) } +func (n *RuntimeGoNakamaModule) LinkCustom(ctx context.Context, userID, customID string) error { + id, err := uuid.FromString(userID) + if err != nil { + return errors.New("user ID must be a valid identifier") + } + + return LinkCustom(ctx, n.logger, n.db, id, customID) +} + +func (n *RuntimeGoNakamaModule) LinkDevice(ctx context.Context, userID, deviceID string) error { + id, err := uuid.FromString(userID) + if err != nil { + return errors.New("user ID must be a valid identifier") + } + + return LinkDevice(ctx, n.logger, n.db, id, deviceID) +} + +func (n *RuntimeGoNakamaModule) LinkEmail(ctx context.Context, userID, email, password string) error { + id, err := uuid.FromString(userID) + if err != nil { + return errors.New("user ID must be a valid identifier") + } + + return LinkEmail(ctx, n.logger, n.db, id, email, password) +} + +func (n *RuntimeGoNakamaModule) LinkFacebook(ctx context.Context, userID, username, token string, importFriends bool) error { + id, err := uuid.FromString(userID) + if err != nil { + return errors.New("user ID must be a valid identifier") + } + + return LinkFacebook(ctx, n.logger, n.db, n.socialClient, n.router, id, username, token, importFriends) +} + +func (n *RuntimeGoNakamaModule) LinkFacebookInstantGame(ctx context.Context, userID, signedPlayerInfo string) error { + id, err := uuid.FromString(userID) + if err != nil { + return errors.New("user ID must be a valid identifier") + } + + return LinkFacebookInstantGame(ctx, n.logger, n.db, n.config, n.socialClient, id, signedPlayerInfo) +} + +func (n *RuntimeGoNakamaModule) LinkGameCenter(ctx context.Context, userID, playerID, bundleID string, timestamp int64, salt, signature, publicKeyUrl string) error { + id, err := uuid.FromString(userID) + if err != nil { + return errors.New("user ID must be a valid identifier") + } + + return LinkGameCenter(ctx, n.logger, n.db, n.socialClient, id, playerID, bundleID, timestamp, salt, signature, publicKeyUrl) +} + +func (n *RuntimeGoNakamaModule) LinkGoogle(ctx context.Context, userID, token string) error { + id, err := uuid.FromString(userID) + if err != nil { + return errors.New("user ID must be a valid identifier") + } + + return LinkGoogle(ctx, n.logger, n.db, n.socialClient, id, token) +} + +func (n *RuntimeGoNakamaModule) LinkSteam(ctx context.Context, userID, token string) error { + id, err := uuid.FromString(userID) + if err != nil { + return errors.New("user ID must be a valid identifier") + } + + return LinkSteam(ctx, n.logger, n.db, n.config, n.socialClient, id, token) +} + +func (n *RuntimeGoNakamaModule) UnlinkCustom(ctx context.Context, userID, customID string) error { + id, err := uuid.FromString(userID) + if err != nil { + return errors.New("user ID must be a valid identifier") + } + + return UnlinkCustom(ctx, n.logger, n.db, id, customID) +} + +func (n *RuntimeGoNakamaModule) UnlinkDevice(ctx context.Context, userID, deviceID string) error { + id, err := uuid.FromString(userID) + if err != nil { + return errors.New("user ID must be a valid identifier") + } + + return UnlinkDevice(ctx, n.logger, n.db, id, deviceID) +} + +func (n *RuntimeGoNakamaModule) UnlinkEmail(ctx context.Context, userID, email string) error { + id, err := uuid.FromString(userID) + if err != nil { + return errors.New("user ID must be a valid identifier") + } + + return UnlinkEmail(ctx, n.logger, n.db, id, email) +} + +func (n *RuntimeGoNakamaModule) UnlinkFacebook(ctx context.Context, userID, token string) error { + id, err := uuid.FromString(userID) + if err != nil { + return errors.New("user ID must be a valid identifier") + } + + return UnlinkFacebook(ctx, n.logger, n.db, n.socialClient, id, token) +} + +func (n *RuntimeGoNakamaModule) UnlinkFacebookInstantGame(ctx context.Context, userID, signedPlayerInfo string) error { + id, err := uuid.FromString(userID) + if err != nil { + return errors.New("user ID must be a valid identifier") + } + + return UnlinkFacebookInstantGame(ctx, n.logger, n.db, n.config, n.socialClient, id, signedPlayerInfo) +} + +func (n *RuntimeGoNakamaModule) UnlinkGameCenter(ctx context.Context, userID, playerID, bundleID string, timestamp int64, salt, signature, publicKeyUrl string) error { + id, err := uuid.FromString(userID) + if err != nil { + return errors.New("user ID must be a valid identifier") + } + + return UnlinkGameCenter(ctx, n.logger, n.db, n.socialClient, id, playerID, bundleID, timestamp, salt, signature, publicKeyUrl) +} + +func (n *RuntimeGoNakamaModule) UnlinkGoogle(ctx context.Context, userID, token string) error { + id, err := uuid.FromString(userID) + if err != nil { + return errors.New("user ID must be a valid identifier") + } + + return UnlinkGoogle(ctx, n.logger, n.db, n.socialClient, id, token) +} + +func (n *RuntimeGoNakamaModule) UnlinkSteam(ctx context.Context, userID, token string) error { + id, err := uuid.FromString(userID) + if err != nil { + return errors.New("user ID must be a valid identifier") + } + + return UnlinkSteam(ctx, n.logger, n.db, n.config, n.socialClient, id, token) +} + func (n *RuntimeGoNakamaModule) StreamUserList(mode uint8, subject, subcontext, label string, includeHidden, includeNotHidden bool) ([]runtime.Presence, error) { stream := PresenceStream{ Mode: mode, diff --git a/server/runtime_lua_nakama.go b/server/runtime_lua_nakama.go index 8fc05c47d879bc86ebca48b8eb9e127c4a556369..e9d865b640d8de7249203ab3dce115ab2a8d0304 100644 --- a/server/runtime_lua_nakama.go +++ b/server/runtime_lua_nakama.go @@ -175,6 +175,22 @@ func (n *RuntimeLuaNakamaModule) Loader(l *lua.LState) int { "users_get_username": n.usersGetUsername, "users_ban_id": n.usersBanId, "users_unban_id": n.usersUnbanId, + "link_custom": n.linkCustom, + "link_device": n.linkDevice, + "link_email": n.linkEmail, + "link_facebook": n.linkFacebook, + "link_facebook_instant_game": n.linkFacebook, + "link_gamecenter": n.linkGameCenter, + "link_google": n.linkGoogle, + "link_steam": n.linkSteam, + "unlink_custom": n.unlinkCustom, + "unlink_device": n.unlinkDevice, + "unlink_email": n.unlinkEmail, + "unlink_facebook": n.unlinkFacebook, + "unlink_facebook_instant_game": n.unlinkFacebook, + "unlink_gamecenter": n.unlinkGameCenter, + "unlink_google": n.unlinkGoogle, + "unlink_steam": n.unlinkSteam, "stream_user_list": n.streamUserList, "stream_user_get": n.streamUserGet, "stream_user_join": n.streamUserJoin, @@ -2118,6 +2134,387 @@ func (n *RuntimeLuaNakamaModule) usersUnbanId(l *lua.LState) int { return 0 } +func (n *RuntimeLuaNakamaModule) linkCustom(l *lua.LState) int { + userID := l.CheckString(1) + id, err := uuid.FromString(userID) + if err != nil { + l.ArgError(1, "user ID must be a valid identifier") + return 0 + } + + customID := l.CheckString(2) + if customID == "" { + l.ArgError(2, "expects custom ID string") + return 0 + } + + if err := LinkCustom(l.Context(), n.logger, n.db, id, customID); err != nil { + l.RaiseError("error linking: %v", err.Error()) + } + return 0 +} + +func (n *RuntimeLuaNakamaModule) linkDevice(l *lua.LState) int { + userID := l.CheckString(1) + id, err := uuid.FromString(userID) + if err != nil { + l.ArgError(1, "user ID must be a valid identifier") + return 0 + } + + deviceID := l.CheckString(2) + if deviceID == "" { + l.ArgError(2, "expects device ID string") + return 0 + } + + if err := LinkDevice(l.Context(), n.logger, n.db, id, deviceID); err != nil { + l.RaiseError("error linking: %v", err.Error()) + } + return 0 +} + +func (n *RuntimeLuaNakamaModule) linkEmail(l *lua.LState) int { + userID := l.CheckString(1) + id, err := uuid.FromString(userID) + if err != nil { + l.ArgError(1, "user ID must be a valid identifier") + return 0 + } + + email := l.CheckString(2) + if email == "" { + l.ArgError(2, "expects email string") + return 0 + } + password := l.CheckString(3) + if password == "" { + l.ArgError(3, "expects username string") + return 0 + } + + if err := LinkEmail(l.Context(), n.logger, n.db, id, email, password); err != nil { + l.RaiseError("error linking: %v", err.Error()) + } + return 0 +} + +func (n *RuntimeLuaNakamaModule) linkFacebook(l *lua.LState) int { + userID := l.CheckString(1) + id, err := uuid.FromString(userID) + if err != nil { + l.ArgError(1, "user ID must be a valid identifier") + return 0 + } + + username := l.CheckString(2) + if username == "" { + l.ArgError(2, "expects username string") + return 0 + } + token := l.CheckString(3) + if token == "" { + l.ArgError(3, "expects token string") + return 0 + } + importFriends := l.OptBool(4, true) + + if err := LinkFacebook(l.Context(), n.logger, n.db, n.socialClient, n.router, id, username, token, importFriends); err != nil { + l.RaiseError("error linking: %v", err.Error()) + } + return 0 +} + +func (n *RuntimeLuaNakamaModule) linkFacebookInstantGame(l *lua.LState) int { + userID := l.CheckString(1) + id, err := uuid.FromString(userID) + if err != nil { + l.ArgError(1, "user ID must be a valid identifier") + return 0 + } + + signedPlayerInfo := l.CheckString(2) + if signedPlayerInfo == "" { + l.ArgError(2, "expects signed player info string") + return 0 + } + + if err := LinkFacebookInstantGame(l.Context(), n.logger, n.db, n.config, n.socialClient, id, signedPlayerInfo); err != nil { + l.RaiseError("error linking: %v", err.Error()) + } + return 0 +} + +func (n *RuntimeLuaNakamaModule) linkGameCenter(l *lua.LState) int { + userID := l.CheckString(1) + id, err := uuid.FromString(userID) + if err != nil { + l.ArgError(1, "user ID must be a valid identifier") + return 0 + } + + playerID := l.CheckString(2) + if playerID == "" { + l.ArgError(2, "expects player ID string") + return 0 + } + bundleID := l.CheckString(3) + if bundleID == "" { + l.ArgError(3, "expects bundle ID string") + return 0 + } + ts := l.CheckInt64(4) + if ts == 0 { + l.ArgError(4, "expects timestamp value") + return 0 + } + salt := l.CheckString(5) + if salt == "" { + l.ArgError(5, "expects salt string") + return 0 + } + signature := l.CheckString(6) + if signature == "" { + l.ArgError(6, "expects signature string") + return 0 + } + publicKeyURL := l.CheckString(7) + if publicKeyURL == "" { + l.ArgError(7, "expects public key URL string") + return 0 + } + + if err := LinkGameCenter(l.Context(), n.logger, n.db, n.socialClient, id, playerID, bundleID, ts, salt, signature, publicKeyURL); err != nil { + l.RaiseError("error linking: %v", err.Error()) + } + return 0 +} + +func (n *RuntimeLuaNakamaModule) linkGoogle(l *lua.LState) int { + userID := l.CheckString(1) + id, err := uuid.FromString(userID) + if err != nil { + l.ArgError(1, "user ID must be a valid identifier") + return 0 + } + + token := l.CheckString(2) + if token == "" { + l.ArgError(2, "expects token string") + return 0 + } + + if err := LinkGoogle(l.Context(), n.logger, n.db, n.socialClient, id, token); err != nil { + l.RaiseError("error linking: %v", err.Error()) + } + return 0 +} + +func (n *RuntimeLuaNakamaModule) linkSteam(l *lua.LState) int { + userID := l.CheckString(1) + id, err := uuid.FromString(userID) + if err != nil { + l.ArgError(1, "user ID must be a valid identifier") + return 0 + } + + token := l.CheckString(2) + if token == "" { + l.ArgError(2, "expects token string") + return 0 + } + + if err := LinkSteam(l.Context(), n.logger, n.db, n.config, n.socialClient, id, token); err != nil { + l.RaiseError("error linking: %v", err.Error()) + } + return 0 +} + +func (n *RuntimeLuaNakamaModule) unlinkCustom(l *lua.LState) int { + userID := l.CheckString(1) + id, err := uuid.FromString(userID) + if err != nil { + l.ArgError(1, "user ID must be a valid identifier") + return 0 + } + + customID := l.CheckString(2) + if customID == "" { + l.ArgError(2, "expects custom ID string") + return 0 + } + + if err := UnlinkCustom(l.Context(), n.logger, n.db, id, customID); err != nil { + l.RaiseError("error unlinking: %v", err.Error()) + } + return 0 +} + +func (n *RuntimeLuaNakamaModule) unlinkDevice(l *lua.LState) int { + userID := l.CheckString(1) + id, err := uuid.FromString(userID) + if err != nil { + l.ArgError(1, "user ID must be a valid identifier") + return 0 + } + + deviceID := l.CheckString(2) + if deviceID == "" { + l.ArgError(2, "expects device ID string") + return 0 + } + + if err := UnlinkDevice(l.Context(), n.logger, n.db, id, deviceID); err != nil { + l.RaiseError("error unlinking: %v", err.Error()) + } + return 0 +} + +func (n *RuntimeLuaNakamaModule) unlinkEmail(l *lua.LState) int { + userID := l.CheckString(1) + id, err := uuid.FromString(userID) + if err != nil { + l.ArgError(1, "user ID must be a valid identifier") + return 0 + } + + email := l.CheckString(2) + if email == "" { + l.ArgError(2, "expects email string") + return 0 + } + + if err := UnlinkEmail(l.Context(), n.logger, n.db, id, email); err != nil { + l.RaiseError("error unlinking: %v", err.Error()) + } + return 0 +} + +func (n *RuntimeLuaNakamaModule) unlinkFacebook(l *lua.LState) int { + userID := l.CheckString(1) + id, err := uuid.FromString(userID) + if err != nil { + l.ArgError(1, "user ID must be a valid identifier") + return 0 + } + + token := l.CheckString(2) + if token == "" { + l.ArgError(2, "expects token string") + return 0 + } + + if err := UnlinkFacebook(l.Context(), n.logger, n.db, n.socialClient, id, token); err != nil { + l.RaiseError("error unlinking: %v", err.Error()) + } + return 0 +} + +func (n *RuntimeLuaNakamaModule) unlinkFacebookInstantGame(l *lua.LState) int { + userID := l.CheckString(1) + id, err := uuid.FromString(userID) + if err != nil { + l.ArgError(1, "user ID must be a valid identifier") + return 0 + } + + signedPlayerInfo := l.CheckString(2) + if signedPlayerInfo == "" { + l.ArgError(2, "expects signed player info string") + return 0 + } + + if err := UnlinkFacebookInstantGame(l.Context(), n.logger, n.db, n.config, n.socialClient, id, signedPlayerInfo); err != nil { + l.RaiseError("error unlinking: %v", err.Error()) + } + return 0 +} + +func (n *RuntimeLuaNakamaModule) unlinkGameCenter(l *lua.LState) int { + userID := l.CheckString(1) + id, err := uuid.FromString(userID) + if err != nil { + l.ArgError(1, "user ID must be a valid identifier") + return 0 + } + + playerID := l.CheckString(2) + if playerID == "" { + l.ArgError(2, "expects player ID string") + return 0 + } + bundleID := l.CheckString(3) + if bundleID == "" { + l.ArgError(3, "expects bundle ID string") + return 0 + } + ts := l.CheckInt64(4) + if ts == 0 { + l.ArgError(4, "expects timestamp value") + return 0 + } + salt := l.CheckString(5) + if salt == "" { + l.ArgError(5, "expects salt string") + return 0 + } + signature := l.CheckString(6) + if signature == "" { + l.ArgError(6, "expects signature string") + return 0 + } + publicKeyURL := l.CheckString(7) + if publicKeyURL == "" { + l.ArgError(7, "expects public key URL string") + return 0 + } + + if err := UnlinkGameCenter(l.Context(), n.logger, n.db, n.socialClient, id, playerID, bundleID, ts, salt, signature, publicKeyURL); err != nil { + l.RaiseError("error unlinking: %v", err.Error()) + } + return 0 +} + +func (n *RuntimeLuaNakamaModule) unlinkGoogle(l *lua.LState) int { + userID := l.CheckString(1) + id, err := uuid.FromString(userID) + if err != nil { + l.ArgError(1, "user ID must be a valid identifier") + return 0 + } + + token := l.CheckString(2) + if token == "" { + l.ArgError(2, "expects token string") + return 0 + } + + if err := UnlinkGoogle(l.Context(), n.logger, n.db, n.socialClient, id, token); err != nil { + l.RaiseError("error unlinking: %v", err.Error()) + } + return 0 +} + +func (n *RuntimeLuaNakamaModule) unlinkSteam(l *lua.LState) int { + userID := l.CheckString(1) + id, err := uuid.FromString(userID) + if err != nil { + l.ArgError(1, "user ID must be a valid identifier") + return 0 + } + + token := l.CheckString(2) + if token == "" { + l.ArgError(2, "expects token string") + return 0 + } + + if err := UnlinkSteam(l.Context(), n.logger, n.db, n.config, n.socialClient, id, token); err != nil { + l.RaiseError("error unlinking: %v", err.Error()) + } + return 0 +} + func (n *RuntimeLuaNakamaModule) streamUserList(l *lua.LState) int { // Parse input stream identifier. streamTable := l.CheckTable(1) diff --git a/vendor/github.com/heroiclabs/nakama-common/api/api.pb.go b/vendor/github.com/heroiclabs/nakama-common/api/api.pb.go index 30e6386ad324e1f65fd0cf3d670ba8a76cbbbfb3..0cf66d21a8e3827be5782e5b8a67cf4d876b7f83 100644 --- a/vendor/github.com/heroiclabs/nakama-common/api/api.pb.go +++ b/vendor/github.com/heroiclabs/nakama-common/api/api.pb.go @@ -143,7 +143,9 @@ type Account struct { // The custom id in the user's account. CustomId string `protobuf:"bytes,5,opt,name=custom_id,json=customId,proto3" json:"custom_id,omitempty"` // The UNIX time when the user's email was verified. - VerifyTime *timestamp.Timestamp `protobuf:"bytes,6,opt,name=verify_time,json=verifyTime,proto3" json:"verify_time,omitempty"` + VerifyTime *timestamp.Timestamp `protobuf:"bytes,6,opt,name=verify_time,json=verifyTime,proto3" json:"verify_time,omitempty"` + // The UNIX time when the user's account was disabled/banned. + DisableTime *timestamp.Timestamp `protobuf:"bytes,7,opt,name=disable_time,json=disableTime,proto3" json:"disable_time,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -216,6 +218,13 @@ func (m *Account) GetVerifyTime() *timestamp.Timestamp { return nil } +func (m *Account) GetDisableTime() *timestamp.Timestamp { + if m != nil { + return m.DisableTime + } + return nil +} + // Send a custom ID to the server. Used with authenticate/link/unlink. type AccountCustom struct { // A custom identifier. @@ -5907,241 +5916,240 @@ func init() { proto.RegisterType((*WriteTournamentRecordRequest_TournamentRecordWrite)(nil), "nakama.api.WriteTournamentRecordRequest.TournamentRecordWrite") } -func init() { - proto.RegisterFile("api/api.proto", fileDescriptor_1b40cafcd4234784) -} +func init() { proto.RegisterFile("api/api.proto", fileDescriptor_1b40cafcd4234784) } var fileDescriptor_1b40cafcd4234784 = []byte{ - // 3689 bytes of a gzipped FileDescriptorProto + // 3703 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x3b, 0x4b, 0x8c, 0x1b, 0xc7, 0xb1, 0x1e, 0xfe, 0x59, 0x5c, 0xee, 0x72, 0x5b, 0x9f, 0xc7, 0xa5, 0xb4, 0xfa, 0x8c, 0x6c, 0x4b, 0x86, 0xfd, 0x28, 0x7b, 0x65, 0x4b, 0xb2, 0xf5, 0x2c, 0x6b, 0x3f, 0x94, 0x40, 0x4b, 0x5a, 0xe9, 0xcd, 0x4a, 0x7a, 0x0f, 0xef, 0x1d, 0xe8, 0xde, 0x99, 0xde, 0xd5, 0xbc, 0x25, 0x67, 0xe8, 0x99, 0xe1, 0x7e, 0x8c, 0xf7, 0x80, 0x87, 0x20, 0x40, 0x72, 0x0a, 0x92, 0x00, 0x49, 0x10, 0x04, 0x48, - 0x82, 0x1c, 0x02, 0x3b, 0x87, 0x20, 0x39, 0xe4, 0x12, 0xe4, 0xee, 0x04, 0xb9, 0x05, 0x71, 0x90, - 0x5b, 0x12, 0xe4, 0x14, 0x20, 0xb7, 0x1c, 0x73, 0x09, 0xba, 0xba, 0xe7, 0x4b, 0x72, 0x97, 0xdc, - 0xa5, 0x6c, 0x23, 0x97, 0x05, 0xbb, 0xba, 0xaa, 0xa7, 0xaa, 0xba, 0xba, 0xaa, 0xba, 0xaa, 0x17, - 0xca, 0xb4, 0x6b, 0x5e, 0xa6, 0x5d, 0xb3, 0xde, 0x75, 0x6c, 0xcf, 0x26, 0x60, 0xd1, 0x2d, 0xda, - 0xa1, 0x75, 0xda, 0x35, 0x6b, 0x67, 0x37, 0x6d, 0x7b, 0xb3, 0xcd, 0x2e, 0xe3, 0xcc, 0x7a, 0x6f, - 0xe3, 0xb2, 0x67, 0x76, 0x98, 0xeb, 0xd1, 0x4e, 0x57, 0x20, 0xd7, 0xce, 0x24, 0x11, 0x76, 0x1c, - 0xda, 0xed, 0x32, 0xc7, 0x15, 0xf3, 0xea, 0x5f, 0x15, 0xc8, 0x2f, 0xea, 0xba, 0xdd, 0xb3, 0x3c, - 0xf2, 0x3c, 0x64, 0x7a, 0x2e, 0x73, 0xaa, 0xca, 0x39, 0xe5, 0x52, 0x69, 0xa1, 0x52, 0x0f, 0xbf, - 0x53, 0x7f, 0xec, 0x32, 0x47, 0xc3, 0x59, 0x72, 0x12, 0x72, 0x3b, 0xb4, 0xdd, 0x66, 0x5e, 0x35, - 0x75, 0x4e, 0xb9, 0x54, 0xd4, 0xe4, 0x88, 0x1c, 0x87, 0x2c, 0xeb, 0x50, 0xb3, 0x5d, 0x4d, 0x23, - 0x58, 0x0c, 0xc8, 0x15, 0xc8, 0x1b, 0x6c, 0xdb, 0xd4, 0x99, 0x5b, 0xcd, 0x9c, 0x4b, 0x5f, 0x2a, - 0x2d, 0xcc, 0x45, 0x97, 0x95, 0x5f, 0x5e, 0x41, 0x0c, 0xcd, 0xc7, 0x24, 0xa7, 0xa0, 0xa8, 0xf7, - 0x5c, 0xcf, 0xee, 0xb4, 0x4c, 0xa3, 0x9a, 0xc5, 0xe5, 0x0a, 0x02, 0xd0, 0x34, 0xc8, 0x0d, 0x28, - 0x6d, 0x33, 0xc7, 0xdc, 0xd8, 0x6b, 0x71, 0x59, 0xab, 0x39, 0x64, 0xb6, 0x56, 0x17, 0x72, 0xd6, - 0x7d, 0x39, 0xeb, 0x8f, 0x7c, 0x45, 0x68, 0x20, 0xd0, 0x39, 0x40, 0xfd, 0x9a, 0x02, 0x65, 0xf9, - 0xd1, 0x65, 0x5c, 0x90, 0x4c, 0x43, 0xca, 0x34, 0x50, 0xe4, 0xa2, 0x96, 0x32, 0x0d, 0x72, 0x0d, - 0x32, 0xdb, 0xd4, 0x71, 0xab, 0x29, 0xe4, 0xf6, 0xc2, 0x00, 0x6e, 0x05, 0x61, 0xfd, 0x09, 0x75, - 0xdc, 0x86, 0xe5, 0x39, 0x7b, 0x1a, 0x12, 0xd4, 0xae, 0x41, 0x31, 0x00, 0x91, 0x0a, 0xa4, 0xb7, - 0xd8, 0x9e, 0x5c, 0x96, 0xff, 0xe4, 0xea, 0xd9, 0xa6, 0xed, 0x1e, 0x93, 0x5a, 0x13, 0x83, 0xb7, - 0x52, 0xd7, 0x95, 0x28, 0x4f, 0x42, 0x11, 0x87, 0xe0, 0x49, 0x10, 0x4e, 0x8e, 0xa7, 0x9f, 0x2a, - 0x30, 0x25, 0x97, 0x6e, 0xe0, 0x3e, 0x06, 0xbb, 0xab, 0x44, 0x77, 0xb7, 0x06, 0x85, 0x2e, 0x75, - 0xdd, 0x1d, 0xdb, 0x31, 0xe4, 0x1a, 0xc1, 0x98, 0x5c, 0x95, 0x4c, 0xa7, 0x91, 0x69, 0x75, 0x00, - 0xd3, 0xb8, 0xf2, 0xe4, 0x78, 0xfe, 0x8e, 0x02, 0x33, 0x72, 0xe5, 0xdb, 0x54, 0x67, 0xeb, 0xb6, - 0xbd, 0xc5, 0xb1, 0x3d, 0x7b, 0x8b, 0x59, 0x3e, 0xdb, 0x38, 0x20, 0x6f, 0xc6, 0xf4, 0xf9, 0xc2, - 0x00, 0xd6, 0xfc, 0x05, 0x26, 0xc7, 0xdd, 0xaf, 0x14, 0xa8, 0x25, 0x16, 0x6f, 0x5a, 0xae, 0x47, - 0x2d, 0xef, 0x0e, 0xed, 0x30, 0xf2, 0x0a, 0x10, 0xd7, 0xdc, 0xb4, 0x98, 0xd1, 0xea, 0xb6, 0xe9, - 0x1e, 0x73, 0x5a, 0xa6, 0xb5, 0x61, 0xcb, 0x95, 0x2b, 0x62, 0xe6, 0x21, 0x4e, 0x34, 0xad, 0x0d, - 0x9b, 0xac, 0xc4, 0x04, 0x78, 0x75, 0x1f, 0x01, 0x22, 0xdf, 0x98, 0x9c, 0x2c, 0xbf, 0x4c, 0xc1, - 0xac, 0xfc, 0x0e, 0x5f, 0x78, 0x99, 0x59, 0x1e, 0x73, 0xf8, 0xa9, 0xf5, 0x79, 0xf7, 0x8d, 0xb7, - 0x20, 0x00, 0x4d, 0x83, 0x4f, 0xae, 0xf7, 0x2c, 0xa3, 0xcd, 0xf8, 0xa4, 0x34, 0x15, 0x01, 0x68, - 0x1a, 0xe4, 0x65, 0x98, 0x0d, 0xfc, 0x56, 0xcb, 0x65, 0xba, 0x6d, 0x19, 0x2e, 0xba, 0x91, 0xb4, - 0x56, 0x09, 0x26, 0xd6, 0x04, 0x9c, 0x10, 0xc8, 0xb8, 0xb4, 0xed, 0x55, 0x33, 0xb8, 0x08, 0xfe, - 0x26, 0xa7, 0xa1, 0xc8, 0x75, 0x44, 0xbd, 0x9e, 0xc3, 0xa4, 0xc3, 0x08, 0x01, 0xe4, 0x79, 0x98, - 0xee, 0xf6, 0xd6, 0xdb, 0xa6, 0xde, 0xda, 0x62, 0x7b, 0xad, 0x9e, 0xd3, 0x46, 0xa7, 0x51, 0xd4, - 0xa6, 0x04, 0xf4, 0x2e, 0xdb, 0x7b, 0xec, 0xb4, 0xc9, 0x0d, 0xa9, 0xd3, 0x3c, 0xea, 0xf4, 0xe2, - 0x00, 0x9d, 0x86, 0xb2, 0x4e, 0x4e, 0x95, 0xdf, 0x0a, 0x0f, 0xff, 0x1d, 0xf4, 0x60, 0x43, 0x4c, - 0xf6, 0x60, 0x17, 0x20, 0xc8, 0x27, 0xc7, 0xd9, 0x37, 0x42, 0x17, 0xb0, 0xe6, 0x31, 0xda, 0x19, - 0xc2, 0xd8, 0xd5, 0x18, 0x63, 0x83, 0x8e, 0x39, 0x52, 0x4f, 0x8e, 0xaf, 0x65, 0x98, 0x5d, 0x34, - 0x8c, 0xdb, 0x8e, 0xc9, 0x2c, 0xc3, 0xd5, 0xd8, 0xfb, 0x3d, 0xe6, 0x7a, 0x7c, 0x01, 0xd3, 0x70, - 0xab, 0xca, 0xb9, 0x34, 0x5f, 0xc0, 0x34, 0x5c, 0x6e, 0x12, 0x3c, 0x5c, 0x59, 0xb4, 0xc3, 0x04, - 0x73, 0x45, 0x2d, 0x04, 0xa8, 0xf7, 0xe0, 0xf8, 0xa2, 0x61, 0xdc, 0x71, 0xec, 0x5e, 0x97, 0x87, - 0xb6, 0x60, 0x9d, 0x39, 0x28, 0x6c, 0x72, 0x60, 0x68, 0xc2, 0x79, 0x1c, 0x37, 0x0d, 0x3e, 0xc5, - 0xe9, 0x5b, 0xfc, 0x3b, 0x62, 0xbd, 0x3c, 0x1f, 0x37, 0x0d, 0x57, 0xfd, 0xbe, 0x02, 0x73, 0x8b, - 0x3d, 0xef, 0x29, 0xb3, 0x3c, 0x53, 0xa7, 0x1e, 0x13, 0x11, 0xc2, 0x5f, 0xf3, 0x0a, 0xe4, 0xa9, - 0xd0, 0x84, 0x8c, 0xac, 0x73, 0x43, 0x83, 0x8a, 0xe6, 0x63, 0x92, 0x05, 0xc8, 0xe9, 0x0e, 0xa3, - 0x9e, 0x50, 0xc0, 0xa0, 0x00, 0xb7, 0x64, 0xdb, 0xed, 0x27, 0x5c, 0x2b, 0x9a, 0xc4, 0xe4, 0xde, - 0xd8, 0x97, 0x50, 0x06, 0xe1, 0x60, 0xdc, 0xc7, 0xa2, 0x0c, 0xb9, 0xe3, 0xb0, 0xe8, 0x47, 0xe9, - 0x67, 0xc5, 0xe2, 0x77, 0x15, 0xa8, 0x46, 0x59, 0xc4, 0xf0, 0xe0, 0x73, 0xb8, 0x90, 0xe4, 0xb0, - 0x3a, 0x2c, 0xa0, 0x3c, 0x3b, 0x06, 0x7f, 0xab, 0xc0, 0xa9, 0x28, 0x83, 0xbe, 0x8f, 0xf5, 0x79, - 0x7c, 0x23, 0xc9, 0xe3, 0xa9, 0x7d, 0x1c, 0xf3, 0x33, 0x63, 0x93, 0xd4, 0x21, 0xe3, 0xee, 0x59, - 0x3a, 0x3a, 0xc8, 0xfd, 0x57, 0x43, 0x3c, 0xf5, 0xe7, 0x0a, 0xbc, 0x38, 0x48, 0xac, 0x48, 0xe8, - 0xf0, 0x25, 0xbc, 0x95, 0x94, 0xf0, 0xc5, 0xd1, 0x42, 0xcf, 0xb3, 0xdb, 0x93, 0x0f, 0x15, 0x98, - 0x8f, 0x32, 0x1f, 0xfa, 0x68, 0x9f, 0xe7, 0x6b, 0x49, 0x9e, 0xe7, 0xf7, 0x75, 0xed, 0x9f, 0xde, - 0x11, 0x14, 0x0e, 0x7b, 0xac, 0x23, 0x28, 0x49, 0x3e, 0xb5, 0x23, 0x88, 0xae, 0x7b, 0xac, 0x23, - 0x28, 0x28, 0x9e, 0x19, 0x83, 0xf7, 0xe0, 0xf8, 0x12, 0xb5, 0x26, 0xe5, 0xb7, 0x1b, 0x70, 0x6c, - 0xa9, 0x6d, 0xeb, 0x5b, 0x47, 0x0c, 0x26, 0xff, 0x9f, 0x81, 0xe9, 0xe5, 0xa7, 0xd4, 0xb2, 0x58, - 0xfb, 0x3e, 0x73, 0x5d, 0xba, 0xc9, 0xc8, 0x3c, 0x80, 0x2e, 0x20, 0x21, 0x47, 0x45, 0x09, 0x69, - 0x1a, 0x7c, 0xba, 0x23, 0x30, 0xc3, 0x74, 0xa8, 0x28, 0x21, 0x4d, 0x83, 0x5c, 0x86, 0x8c, 0x6e, - 0x1b, 0x42, 0x7a, 0xee, 0x45, 0x92, 0x3a, 0x6b, 0x5a, 0xde, 0x95, 0x05, 0x79, 0x84, 0x39, 0x22, - 0xcf, 0xae, 0x5c, 0x66, 0x19, 0x22, 0xf5, 0x12, 0x89, 0x51, 0x41, 0x00, 0x9a, 0x46, 0x4c, 0x9f, - 0xd9, 0x84, 0xaf, 0xa8, 0x42, 0x5e, 0xb7, 0x2d, 0x8f, 0x59, 0x9e, 0xcc, 0x89, 0xfc, 0x21, 0xbf, - 0x66, 0x89, 0xfd, 0x10, 0xd7, 0xac, 0xfc, 0xc1, 0xd7, 0x2c, 0x81, 0xce, 0x01, 0x9c, 0xb8, 0xd7, - 0x35, 0x02, 0xe2, 0xc2, 0xc1, 0xc4, 0x02, 0x1d, 0x89, 0xdf, 0x02, 0xe0, 0x17, 0x54, 0xd3, 0x45, - 0xb6, 0x8a, 0x07, 0xda, 0x4d, 0x04, 0x9b, 0x2b, 0xc2, 0xb1, 0xed, 0x4e, 0x0b, 0x85, 0x05, 0x21, - 0x2c, 0x07, 0xac, 0x72, 0x61, 0xa3, 0x46, 0x52, 0x8a, 0x1b, 0xc9, 0x19, 0x28, 0x49, 0x23, 0x69, - 0xd9, 0x16, 0xab, 0x4e, 0x89, 0x1d, 0x11, 0x76, 0xf2, 0xc0, 0x62, 0xd1, 0x79, 0x6f, 0xc7, 0xae, - 0x96, 0xa3, 0xf3, 0x8f, 0x76, 0x6c, 0xf5, 0x2b, 0x0a, 0x90, 0xb8, 0x09, 0xdc, 0x33, 0x5d, 0x8f, - 0x5c, 0x85, 0x82, 0xdc, 0x55, 0x61, 0x4e, 0x5c, 0x90, 0xc8, 0x99, 0x89, 0x53, 0x68, 0x01, 0x2e, - 0x39, 0x0b, 0x25, 0x8b, 0xed, 0x7a, 0x2d, 0xbd, 0xe7, 0xb8, 0xb6, 0x23, 0x0d, 0x04, 0x38, 0x68, - 0x19, 0x21, 0x1c, 0xa1, 0xeb, 0xb0, 0x6d, 0x1f, 0x41, 0x1c, 0x13, 0xe0, 0x20, 0x81, 0xa0, 0xfe, - 0x8c, 0x33, 0x84, 0x1b, 0x82, 0x87, 0xc5, 0x37, 0x6d, 0x02, 0x19, 0x54, 0x8d, 0xb0, 0x48, 0xfc, - 0x4d, 0xce, 0x41, 0xc9, 0x60, 0xae, 0xee, 0x98, 0x5d, 0xcf, 0xb4, 0x2d, 0xf9, 0xb1, 0x28, 0x88, - 0x2b, 0xae, 0x4d, 0xad, 0xcd, 0x96, 0x47, 0x37, 0xe5, 0xa7, 0xf2, 0x7c, 0xfc, 0x88, 0x6e, 0x72, - 0x4b, 0xa6, 0xdb, 0xd4, 0xa3, 0x0e, 0xe6, 0xd5, 0xc2, 0xf4, 0x8a, 0x02, 0xc2, 0x93, 0x6a, 0x02, - 0x19, 0xbb, 0xcb, 0x2c, 0xb4, 0xbb, 0x82, 0x86, 0xbf, 0xf9, 0x1e, 0x75, 0xe8, 0x6e, 0x4b, 0x78, - 0x12, 0x6e, 0x75, 0x59, 0xad, 0xd0, 0xa1, 0xbb, 0xcb, 0x7c, 0xac, 0xde, 0x86, 0xe3, 0x2b, 0xac, - 0xcd, 0x3c, 0x76, 0xc4, 0x33, 0x79, 0x19, 0x88, 0x58, 0x27, 0x26, 0xfe, 0x70, 0x37, 0xa1, 0xde, - 0x81, 0x33, 0x82, 0xe0, 0x1e, 0xa3, 0x06, 0x73, 0xd6, 0x6d, 0xea, 0x18, 0x1a, 0xd3, 0x6d, 0xfe, - 0x57, 0x10, 0xbf, 0x00, 0xd3, 0xed, 0x70, 0x2e, 0x5c, 0xa2, 0x1c, 0x81, 0x36, 0x0d, 0xb5, 0x0e, - 0x35, 0xb1, 0xd0, 0xaa, 0xed, 0x99, 0x1b, 0xdc, 0x8d, 0x9a, 0xb6, 0x35, 0x5c, 0x0e, 0x55, 0x87, - 0x13, 0x02, 0x7f, 0xcd, 0xb3, 0x1d, 0xba, 0xc9, 0x1e, 0xac, 0xff, 0x0f, 0xd3, 0x3d, 0xb4, 0x49, - 0xd0, 0xed, 0x76, 0x9b, 0xe9, 0xb8, 0x2d, 0xe2, 0x5b, 0x11, 0x88, 0x9f, 0x34, 0xa7, 0xc2, 0xa4, - 0xb9, 0x0a, 0xf9, 0x6d, 0x7e, 0x16, 0x6c, 0xcb, 0xdf, 0x26, 0x39, 0x54, 0x5b, 0x70, 0x6a, 0xc0, - 0x47, 0xdc, 0x30, 0xae, 0x83, 0x8d, 0x90, 0x96, 0xcf, 0x5c, 0x69, 0xe1, 0x7c, 0xd4, 0x52, 0x07, - 0x72, 0xa8, 0x15, 0x6d, 0xf9, 0xcb, 0x55, 0xff, 0xa6, 0x40, 0xb6, 0xb1, 0xcd, 0x8f, 0xe0, 0x20, - 0x13, 0x5b, 0x04, 0xe8, 0x3a, 0x76, 0x97, 0x39, 0x9e, 0xc9, 0xfc, 0xab, 0x42, 0x6c, 0x7d, 0x24, - 0xad, 0x3f, 0x0c, 0x70, 0xc4, 0x4d, 0x21, 0x42, 0x44, 0xae, 0x43, 0x31, 0xb8, 0x0a, 0x4a, 0xc7, - 0xb8, 0x9f, 0x43, 0x09, 0x91, 0xb9, 0xff, 0x63, 0xbb, 0x1e, 0x37, 0x0c, 0x61, 0xa0, 0x05, 0x2d, - 0x18, 0xd7, 0xde, 0x86, 0x99, 0xc4, 0x47, 0xc7, 0xba, 0x8b, 0x7c, 0xa4, 0x40, 0x4e, 0x18, 0xea, - 0x88, 0xc5, 0xb3, 0xd7, 0x20, 0xeb, 0x7a, 0x61, 0x38, 0xdc, 0xd7, 0xb5, 0x0b, 0x4c, 0xf5, 0x36, - 0x64, 0xd7, 0xf8, 0x0f, 0x02, 0x90, 0xbb, 0xad, 0x35, 0x1b, 0xab, 0x2b, 0x95, 0xe7, 0xc8, 0x0c, - 0x94, 0x9a, 0xab, 0x4f, 0x9a, 0x8f, 0x1a, 0xad, 0xb5, 0xc6, 0xea, 0xa3, 0x8a, 0x42, 0x8e, 0xc1, - 0x8c, 0x04, 0x68, 0x8d, 0xe5, 0x46, 0xf3, 0x49, 0x63, 0xa5, 0x92, 0x22, 0x25, 0xc8, 0x2f, 0xdd, - 0x7b, 0xb0, 0x7c, 0xb7, 0xb1, 0x52, 0x49, 0xab, 0x1a, 0x80, 0x60, 0x15, 0x3d, 0xd3, 0x2b, 0x90, - 0xdf, 0x10, 0x27, 0x4c, 0x6e, 0x37, 0x89, 0x72, 0x2c, 0x10, 0x35, 0x1f, 0x85, 0x9c, 0x84, 0x5c, - 0xcc, 0x15, 0xc9, 0x91, 0x6a, 0xc0, 0xcc, 0x1d, 0xe6, 0xc5, 0x22, 0xf1, 0x98, 0x07, 0x95, 0x9c, - 0x87, 0xa9, 0x0d, 0x99, 0x30, 0xa2, 0xf1, 0xa5, 0x11, 0xa1, 0xe4, 0xc3, 0xb8, 0x6d, 0x7d, 0x98, - 0x86, 0x2c, 0x1e, 0xe3, 0xbe, 0xc2, 0x18, 0x0f, 0xb3, 0xdc, 0xc9, 0xd9, 0x4e, 0x24, 0x8e, 0x4a, - 0x48, 0xd3, 0x08, 0x4c, 0x31, 0x3d, 0xdc, 0xdb, 0x65, 0xf6, 0xf7, 0x76, 0xd9, 0xb8, 0xb7, 0xab, - 0x71, 0x7f, 0xee, 0x51, 0x83, 0x7a, 0x54, 0xc6, 0xcb, 0x60, 0x9c, 0xf0, 0x84, 0xf9, 0xa4, 0x27, - 0xac, 0x4b, 0x4f, 0x58, 0x38, 0x38, 0x2b, 0x47, 0x2f, 0x39, 0x0f, 0xc0, 0x8c, 0x4d, 0x26, 0xdd, - 0x64, 0x11, 0xdd, 0x64, 0x91, 0x43, 0xd0, 0x4f, 0xc6, 0x9d, 0x28, 0xc4, 0x9d, 0x68, 0x32, 0x76, - 0x97, 0x8e, 0x12, 0xbb, 0xa7, 0xc6, 0x89, 0xdd, 0xea, 0x2a, 0x14, 0x71, 0xa7, 0xd0, 0xc6, 0x5e, - 0x82, 0x1c, 0x7a, 0x57, 0xdf, 0xc4, 0x66, 0xa3, 0x26, 0x26, 0xfc, 0xb2, 0x44, 0x18, 0x6a, 0x60, - 0xdf, 0x4c, 0x41, 0x39, 0xc8, 0xf6, 0x70, 0xd1, 0x15, 0x28, 0x09, 0x17, 0xce, 0x4d, 0xc8, 0x5f, - 0xf9, 0x42, 0xdf, 0xca, 0x3e, 0x7e, 0x38, 0xd2, 0x60, 0x33, 0x48, 0x1b, 0x87, 0x7d, 0xaf, 0xf6, - 0x43, 0x45, 0x0a, 0xc0, 0xd1, 0x9e, 0xdd, 0x99, 0xbe, 0xe5, 0x9f, 0xe9, 0x69, 0x80, 0xb5, 0xc7, - 0x0f, 0x1b, 0xda, 0xe2, 0xca, 0xfd, 0xe6, 0x6a, 0xe5, 0x39, 0x52, 0x84, 0xac, 0xf8, 0xa9, 0xf0, - 0xe3, 0x7e, 0xbf, 0x71, 0x7f, 0xa9, 0xa1, 0x55, 0x52, 0xa4, 0x02, 0x53, 0xef, 0x3e, 0x68, 0xae, - 0xb6, 0xb4, 0xc6, 0xbf, 0x3f, 0x6e, 0xac, 0x3d, 0xaa, 0xa4, 0xd5, 0x2f, 0x29, 0x70, 0xba, 0xd9, - 0xe9, 0xda, 0x4e, 0x70, 0xdd, 0x4a, 0x04, 0xcc, 0x43, 0x5e, 0x46, 0x5f, 0x85, 0xac, 0xc3, 0x5c, - 0x59, 0xdc, 0xdf, 0xdf, 0x4e, 0x05, 0xa2, 0xfa, 0xaf, 0x50, 0x79, 0xd7, 0x36, 0xad, 0x51, 0xe3, - 0xec, 0xbf, 0xc1, 0x09, 0x8e, 0xfe, 0xc8, 0xee, 0xa1, 0x03, 0xb0, 0x3c, 0x9f, 0xe6, 0x02, 0x94, - 0xbd, 0x00, 0x18, 0x12, 0x4e, 0x85, 0xc0, 0xa6, 0xa1, 0xde, 0x87, 0x13, 0x77, 0x4d, 0x7d, 0x6b, - 0x52, 0x17, 0x80, 0xbf, 0xa4, 0x61, 0xb6, 0x2f, 0xde, 0x8f, 0x18, 0xe8, 0xf9, 0xba, 0xf6, 0x8e, - 0xc5, 0x22, 0xae, 0x27, 0x8f, 0xe3, 0xa6, 0x41, 0xae, 0x27, 0xae, 0x30, 0xa5, 0x85, 0xd3, 0x7d, - 0x8a, 0x5c, 0xf3, 0x1c, 0xd3, 0xda, 0x14, 0xaa, 0x0c, 0x13, 0xf2, 0xe3, 0x90, 0x75, 0x75, 0xdb, - 0x61, 0xe8, 0x98, 0xd2, 0x9a, 0x18, 0x70, 0xbf, 0xe3, 0xf6, 0xd6, 0xc5, 0x44, 0x16, 0x27, 0x82, - 0x31, 0xf7, 0x04, 0x56, 0xaf, 0xd3, 0x12, 0x93, 0x32, 0x9d, 0xb2, 0x7a, 0x9d, 0x35, 0x9f, 0x30, - 0x70, 0x58, 0xf9, 0x84, 0xc3, 0x4a, 0x78, 0x89, 0xc2, 0x51, 0xbc, 0x44, 0x71, 0xac, 0x0c, 0xff, - 0x06, 0x94, 0xd8, 0x6e, 0xd7, 0x74, 0x64, 0x0b, 0x07, 0x0e, 0x26, 0x16, 0xe8, 0x48, 0x4c, 0x20, - 0xe3, 0x50, 0x6b, 0x0b, 0xbd, 0x5a, 0x5a, 0xc3, 0xdf, 0x44, 0x85, 0x32, 0xf7, 0x86, 0xa1, 0x1e, - 0xb8, 0xd7, 0x2a, 0x6b, 0xa5, 0x0e, 0xdd, 0x5d, 0x95, 0xaa, 0x50, 0x7f, 0xa7, 0xc0, 0x89, 0xbe, - 0xbd, 0x46, 0x97, 0x72, 0x0d, 0xf2, 0x0e, 0x8e, 0x7c, 0x77, 0x12, 0xab, 0x10, 0xf4, 0xe7, 0x83, - 0x3e, 0x36, 0x59, 0x82, 0xb2, 0xb0, 0x00, 0x9f, 0x3c, 0x35, 0x0a, 0xf9, 0x14, 0xd2, 0x68, 0x72, - 0x8d, 0x44, 0xaa, 0x9f, 0x3e, 0x28, 0xd5, 0xcf, 0xf4, 0xa5, 0xfa, 0x75, 0xb4, 0xe1, 0xed, 0x91, - 0x33, 0xdd, 0xff, 0x85, 0x63, 0xf7, 0x4c, 0x6b, 0x6b, 0x42, 0xd5, 0xab, 0x71, 0xab, 0x4d, 0xbf, - 0x50, 0xa0, 0xc6, 0xb5, 0x1e, 0xbf, 0xfb, 0x04, 0xe7, 0xf8, 0x80, 0x8b, 0xf3, 0x6b, 0x90, 0x6d, - 0x9b, 0x1d, 0xd3, 0x1b, 0xc9, 0xd7, 0x22, 0x26, 0x79, 0x1d, 0xf2, 0x1b, 0xb6, 0xb3, 0x43, 0x1d, - 0x63, 0x68, 0xda, 0x18, 0xf2, 0xe8, 0xa3, 0x46, 0x02, 0x44, 0x26, 0x16, 0x90, 0xbe, 0xae, 0x00, - 0xe1, 0xec, 0x27, 0xbc, 0x6d, 0xc0, 0x97, 0x32, 0x32, 0x5f, 0xe3, 0x87, 0x8d, 0x08, 0x53, 0xe9, - 0x18, 0x53, 0x0e, 0xcc, 0x72, 0x9e, 0xd0, 0x00, 0xdc, 0xfd, 0xae, 0x7a, 0x43, 0xc2, 0x5e, 0xc8, - 0x7e, 0x7a, 0x54, 0xf6, 0xd5, 0x9f, 0xf0, 0xe3, 0xe4, 0x7f, 0x74, 0x54, 0x57, 0x7c, 0x88, 0xed, - 0x0b, 0xd4, 0x94, 0x3e, 0x84, 0x9a, 0xe2, 0x7b, 0xf7, 0x89, 0x02, 0x97, 0x38, 0xcb, 0x7d, 0x47, - 0xd2, 0x5d, 0x74, 0xec, 0x9e, 0x65, 0x3c, 0x10, 0xe7, 0x72, 0x9c, 0xdb, 0x1e, 0x59, 0x88, 0x4b, - 0xd4, 0xef, 0xe6, 0x1f, 0xf7, 0x8b, 0x14, 0x0d, 0x1c, 0xe9, 0x78, 0xe0, 0xb8, 0x02, 0x39, 0xe1, - 0xea, 0xe4, 0x79, 0x1a, 0x28, 0xee, 0xd5, 0xd7, 0x65, 0xc1, 0x4c, 0xa0, 0xaa, 0x7f, 0x56, 0x60, - 0x7e, 0xb0, 0x5c, 0x63, 0x0a, 0x73, 0x0a, 0x8a, 0x3e, 0x63, 0x7e, 0xa8, 0x2c, 0x48, 0xce, 0xdc, - 0x43, 0xd8, 0xc8, 0xb0, 0x8d, 0x88, 0x48, 0x99, 0x1d, 0x5d, 0xca, 0x3f, 0xa5, 0xc4, 0xc9, 0xbb, - 0x4f, 0x3d, 0xfd, 0x29, 0x3b, 0xca, 0xc9, 0xbb, 0x05, 0x65, 0xda, 0xf3, 0x9e, 0xda, 0x8e, 0xe9, - 0x51, 0xcf, 0xdc, 0x1e, 0xa5, 0x36, 0x19, 0x27, 0xc0, 0x5d, 0xa7, 0xeb, 0xac, 0x3d, 0x52, 0x70, - 0x17, 0xa8, 0x58, 0x0b, 0x32, 0xad, 0x96, 0x6b, 0x7e, 0xc0, 0xf6, 0xdb, 0x5c, 0x9f, 0xd7, 0x7c, - 0xc7, 0xb4, 0xd6, 0xcc, 0x0f, 0x18, 0xd2, 0xd1, 0x5d, 0x41, 0x97, 0x1d, 0x85, 0x8e, 0xee, 0x22, - 0xdd, 0x02, 0x64, 0xdf, 0xef, 0x31, 0x67, 0x4f, 0xbe, 0x90, 0x38, 0x80, 0x47, 0x44, 0x55, 0x77, - 0xa1, 0xca, 0x55, 0x3c, 0xb0, 0x72, 0x71, 0x08, 0x45, 0xbf, 0x04, 0x15, 0x9d, 0xea, 0x4f, 0x19, - 0x5d, 0x6f, 0xb3, 0x78, 0x2d, 0x6b, 0x26, 0x80, 0xcb, 0x20, 0xf6, 0x3d, 0x05, 0xe6, 0xf8, 0xa7, - 0x07, 0xd7, 0x27, 0xfe, 0x05, 0xf2, 0x32, 0x85, 0x93, 0x86, 0x9b, 0x13, 0x19, 0x5c, 0xa2, 0x46, - 0x92, 0xea, 0xab, 0x91, 0x4c, 0xce, 0x68, 0xd5, 0xdf, 0x28, 0x70, 0x91, 0x73, 0x18, 0xcd, 0x5c, - 0x87, 0x39, 0x8f, 0x51, 0x72, 0xd9, 0xcf, 0x85, 0xeb, 0xf8, 0xa3, 0x02, 0xa7, 0x07, 0x0a, 0x35, - 0x96, 0x24, 0x9f, 0x6b, 0xbf, 0xf1, 0x87, 0x14, 0x9c, 0x8c, 0x8b, 0x18, 0x08, 0xb7, 0x0c, 0xd3, - 0x3a, 0xf5, 0xd8, 0xa6, 0xed, 0xec, 0xb5, 0x5c, 0x8f, 0x3a, 0xbe, 0x6d, 0xef, 0xbf, 0x15, 0x65, - 0x9f, 0x66, 0x8d, 0x93, 0x90, 0x77, 0x60, 0x2a, 0x58, 0x84, 0x59, 0xc6, 0x48, 0xbb, 0x59, 0xf2, - 0x29, 0x1a, 0x96, 0x41, 0x6e, 0x00, 0xe0, 0xc7, 0x45, 0x32, 0x9c, 0x1e, 0x81, 0xbc, 0x88, 0xf8, - 0x98, 0x0d, 0x5f, 0x83, 0x02, 0xb3, 0x0c, 0x41, 0x9a, 0x19, 0x81, 0x34, 0xcf, 0x2c, 0x03, 0x09, - 0x83, 0x6d, 0xc9, 0x1d, 0x62, 0x5b, 0x0a, 0xb1, 0x93, 0xf1, 0x63, 0x99, 0x0a, 0xf0, 0x2c, 0x20, - 0x9e, 0x83, 0x0c, 0x3d, 0xb7, 0x9f, 0x6d, 0x22, 0xf0, 0x55, 0x05, 0xb2, 0x18, 0x46, 0xf8, 0x79, - 0xea, 0xf0, 0x1f, 0x91, 0x5c, 0x05, 0xc7, 0x4d, 0x83, 0x3c, 0x3f, 0x28, 0x4a, 0x14, 0x26, 0x11, - 0x09, 0x08, 0x64, 0x82, 0x28, 0x90, 0xd5, 0xf0, 0xb7, 0x7a, 0x1d, 0x8a, 0xc8, 0x11, 0x5e, 0x48, - 0x5e, 0x06, 0xc1, 0x05, 0x1b, 0x58, 0x39, 0x41, 0x3c, 0xcd, 0xc7, 0xe0, 0x47, 0x78, 0x2a, 0xea, - 0xb0, 0xfb, 0x8a, 0x64, 0x55, 0xc8, 0xbb, 0x3d, 0xf4, 0xa7, 0xfe, 0x35, 0x55, 0x0e, 0xa3, 0xdd, - 0x9f, 0x74, 0xbc, 0xfb, 0x43, 0x64, 0x07, 0x4a, 0xb2, 0xd8, 0xdf, 0x64, 0xca, 0x26, 0x9a, 0x4c, - 0x89, 0xcb, 0x64, 0x6e, 0xac, 0xcb, 0xe4, 0x99, 0x58, 0xc7, 0x27, 0x8f, 0x7a, 0x8e, 0x40, 0xd4, - 0xff, 0x83, 0x4a, 0x54, 0x42, 0xd4, 0xd1, 0x4d, 0x28, 0x5b, 0xd1, 0x30, 0x25, 0x35, 0x15, 0xeb, - 0x49, 0x46, 0x89, 0xb4, 0x38, 0xfa, 0x38, 0xb1, 0xe9, 0x21, 0x54, 0x1f, 0x3a, 0x76, 0xc7, 0x96, - 0xcd, 0x84, 0x09, 0xd4, 0x1d, 0xde, 0x83, 0x63, 0x1a, 0xa3, 0xc6, 0xd1, 0x2b, 0xfe, 0x91, 0x03, - 0x96, 0x8e, 0x1e, 0x30, 0xf5, 0xbf, 0x61, 0xae, 0xef, 0x0b, 0x01, 0xd3, 0x37, 0x07, 0x94, 0xfb, - 0xcf, 0x46, 0x15, 0x37, 0x80, 0xb9, 0x68, 0xb1, 0xff, 0x5d, 0x48, 0x6b, 0x5d, 0x7d, 0x90, 0xa1, - 0x75, 0xe9, 0x5e, 0xdb, 0xa6, 0x41, 0x3d, 0x44, 0x0e, 0xb9, 0x2a, 0x9e, 0x7a, 0x5e, 0xb7, 0xc5, - 0xb9, 0x97, 0x96, 0xc6, 0xc7, 0x77, 0xd9, 0x9e, 0xfa, 0x26, 0xe4, 0xd7, 0x98, 0xeb, 0x72, 0xf1, - 0xb8, 0x39, 0xa2, 0x51, 0x88, 0x45, 0x0b, 0x9a, 0x3f, 0x0c, 0x9f, 0x1e, 0xa5, 0x22, 0x4f, 0x8f, - 0xd4, 0xdf, 0xa7, 0xa0, 0x1c, 0xe3, 0x72, 0x82, 0x0a, 0x0c, 0x8b, 0xfe, 0x99, 0x48, 0xd1, 0x3f, - 0xda, 0x61, 0xc9, 0xc6, 0x3a, 0x2c, 0xe4, 0x22, 0xcc, 0x74, 0x99, 0xd3, 0x31, 0x51, 0x94, 0x96, - 0xc3, 0xa8, 0x21, 0x8b, 0x31, 0xd3, 0x21, 0x98, 0xab, 0x95, 0x1b, 0x5e, 0x04, 0x71, 0xc7, 0x31, - 0x3d, 0xd1, 0x5d, 0xcd, 0x6a, 0x91, 0x05, 0xfe, 0x83, 0x83, 0x3f, 0xbb, 0x0a, 0x8d, 0xba, 0x03, - 0x95, 0x98, 0x66, 0x17, 0xf5, 0xad, 0x49, 0xf6, 0xa3, 0xa2, 0x6a, 0xcf, 0xc4, 0xec, 0xb6, 0x01, - 0xb3, 0xc9, 0x0f, 0xbb, 0xe4, 0x55, 0xc8, 0x50, 0x7d, 0xcb, 0xb7, 0xd4, 0xd3, 0x51, 0x4b, 0x4d, - 0x22, 0x6b, 0x88, 0xa9, 0x36, 0x60, 0x3a, 0x6e, 0xfa, 0xe4, 0x0a, 0xe4, 0x85, 0x01, 0xfb, 0xcb, - 0xcc, 0x0d, 0x5d, 0x46, 0xf3, 0x31, 0xd5, 0xf7, 0x12, 0xdc, 0xa0, 0xe7, 0x39, 0xcc, 0x4a, 0x43, - 0x0b, 0xdc, 0x1f, 0x67, 0x00, 0xc2, 0xcc, 0xa4, 0xef, 0x48, 0x71, 0xc3, 0x37, 0xbd, 0x76, 0xd0, - 0x7a, 0xc2, 0x41, 0xb2, 0x87, 0x91, 0xee, 0xef, 0x61, 0xd4, 0xa0, 0xe0, 0xa7, 0x18, 0xa8, 0xe0, - 0xb2, 0x16, 0x8c, 0xc9, 0x3c, 0x80, 0x6b, 0x3b, 0x5e, 0xcb, 0x76, 0x0c, 0xe6, 0xa0, 0x19, 0x97, - 0xb5, 0x22, 0x87, 0x3c, 0xe0, 0x80, 0x20, 0x3a, 0xe5, 0x70, 0x02, 0x7f, 0x8b, 0x30, 0x29, 0xef, - 0x20, 0x79, 0x84, 0x07, 0xd7, 0x8c, 0xbe, 0xd2, 0x5b, 0xa1, 0xaf, 0xf4, 0x86, 0xef, 0xb9, 0xa9, - 0xd5, 0xc2, 0xf7, 0x35, 0x68, 0x88, 0x05, 0xce, 0x8e, 0xd5, 0xc0, 0x67, 0xa3, 0xf3, 0x00, 0x3c, - 0x83, 0xa1, 0x3a, 0x06, 0x59, 0x10, 0xec, 0x30, 0xcb, 0x58, 0x44, 0x00, 0x9f, 0xc6, 0xfa, 0x98, - 0xa8, 0x4a, 0x97, 0xc4, 0x34, 0x87, 0x68, 0x1c, 0x10, 0x2b, 0x70, 0x4e, 0xed, 0x5f, 0xe0, 0x2c, - 0x8f, 0x75, 0x7c, 0xde, 0x8c, 0x65, 0x65, 0xd3, 0x07, 0x37, 0x1c, 0xc3, 0x9c, 0xec, 0x8d, 0x48, - 0x4e, 0x36, 0x73, 0x20, 0x61, 0x90, 0x91, 0xd5, 0xa0, 0x60, 0xf4, 0x1c, 0x0c, 0x4f, 0xd5, 0x8a, - 0xd8, 0x33, 0x7f, 0x4c, 0xce, 0xc3, 0x94, 0xe0, 0x46, 0xaa, 0x69, 0x56, 0x28, 0x19, 0x61, 0x42, - 0x51, 0xea, 0x3a, 0x4c, 0x87, 0x86, 0x84, 0x86, 0x7a, 0x1d, 0x4a, 0x61, 0x9a, 0xee, 0x1b, 0xeb, - 0xc9, 0xa8, 0xb1, 0x46, 0xd2, 0xfe, 0x28, 0xea, 0x50, 0x6b, 0xfd, 0x44, 0x81, 0xe3, 0xc9, 0xab, - 0xc2, 0x3f, 0x43, 0x09, 0xf5, 0xef, 0x29, 0x38, 0xfe, 0x18, 0xbd, 0x9f, 0xac, 0x73, 0xfa, 0x91, - 0x32, 0x5a, 0xc8, 0x57, 0xc6, 0x2a, 0xe4, 0xbf, 0x03, 0x53, 0x86, 0xe9, 0x76, 0xdb, 0x74, 0x4f, - 0x3c, 0x46, 0x49, 0x8d, 0x40, 0x5d, 0x92, 0x14, 0xf8, 0x5a, 0xe5, 0x46, 0xac, 0x9f, 0x38, 0x4a, - 0x7a, 0x19, 0xe9, 0x36, 0x5e, 0x8b, 0xf4, 0x30, 0x33, 0x23, 0x90, 0x06, 0x1d, 0xce, 0xeb, 0x50, - 0x68, 0xdb, 0x22, 0x47, 0x92, 0x97, 0xac, 0x03, 0x04, 0xf6, 0xb1, 0x39, 0x25, 0xb7, 0xf8, 0x0f, - 0x6c, 0x8b, 0x8d, 0x54, 0x72, 0x08, 0xb0, 0xd5, 0x8f, 0x53, 0x40, 0x84, 0xf6, 0x47, 0x2c, 0x61, - 0xf3, 0x80, 0x30, 0xb2, 0x52, 0x45, 0xe5, 0xf3, 0x66, 0xbf, 0xcb, 0x3c, 0x78, 0x37, 0x22, 0x0e, - 0xf5, 0xd0, 0x0a, 0x8d, 0x6f, 0x63, 0x76, 0xbc, 0x6d, 0xf4, 0x9b, 0xc6, 0xb9, 0xd1, 0x9a, 0xc6, - 0xea, 0xaf, 0x33, 0x90, 0xc1, 0xce, 0x65, 0x32, 0x8e, 0x44, 0xdf, 0x80, 0xa5, 0x12, 0x6f, 0xc0, - 0xce, 0x27, 0x2c, 0xd5, 0x0f, 0x27, 0x11, 0x5b, 0x3c, 0xe0, 0x95, 0xcf, 0xfe, 0x1d, 0xf3, 0xc0, - 0x9e, 0x64, 0xc7, 0x3c, 0xb0, 0x98, 0x5a, 0xc4, 0x62, 0x64, 0x73, 0xca, 0x1f, 0xc7, 0xfc, 0x7a, - 0x21, 0xe1, 0xd7, 0xcf, 0x42, 0x29, 0xf2, 0x64, 0x00, 0x03, 0x4a, 0x51, 0x83, 0xf0, 0xc5, 0x00, - 0x8f, 0x37, 0x42, 0x53, 0x7c, 0x5a, 0xbe, 0x02, 0x13, 0x80, 0xa6, 0x41, 0x2e, 0x40, 0x79, 0x93, - 0x76, 0x98, 0x8e, 0xd1, 0x28, 0x7c, 0x0a, 0x36, 0x15, 0x02, 0x45, 0xee, 0xee, 0x7a, 0x8c, 0xe2, - 0x3f, 0x20, 0x4d, 0xc9, 0x4b, 0x13, 0x1f, 0x37, 0xb1, 0x33, 0x60, 0x5b, 0x6d, 0xd3, 0x12, 0x01, - 0xa5, 0xa0, 0xc9, 0x51, 0xa2, 0x61, 0x3f, 0x9d, 0x6c, 0xd8, 0x27, 0x82, 0xd1, 0xcc, 0x51, 0x72, - 0xb9, 0xca, 0x58, 0xdd, 0xb6, 0x6b, 0x50, 0x0d, 0xd5, 0x25, 0xde, 0xe4, 0xb6, 0xb8, 0xb0, 0x5c, - 0xb6, 0x59, 0x94, 0xed, 0xc4, 0x46, 0xff, 0x93, 0xdd, 0xa6, 0xa1, 0x7e, 0x3b, 0x05, 0xe5, 0xe0, - 0x4e, 0xef, 0x37, 0xdf, 0x31, 0x6d, 0x8b, 0xb5, 0xf5, 0x2f, 0x24, 0xfb, 0xe2, 0x01, 0x7e, 0x38, - 0xd2, 0xa0, 0x17, 0x14, 0x07, 0x86, 0x36, 0xdf, 0x3f, 0x52, 0xa0, 0x18, 0x50, 0x90, 0x8b, 0x90, - 0xc5, 0xcf, 0x48, 0xbf, 0x3b, 0xe0, 0xf1, 0x80, 0x98, 0xff, 0x6c, 0xfa, 0xef, 0x97, 0x21, 0x2b, - 0x5e, 0x12, 0xbc, 0x08, 0xd9, 0xe8, 0x4b, 0x84, 0xfe, 0x47, 0x02, 0x62, 0x5a, 0xfd, 0x72, 0x0a, - 0xe6, 0x31, 0xab, 0x3f, 0xe2, 0xfb, 0x32, 0xf2, 0x9f, 0x90, 0x13, 0xb1, 0x52, 0xca, 0x7b, 0x2b, - 0xfa, 0xc5, 0x7d, 0xbf, 0xd0, 0x1f, 0x48, 0x11, 0x5d, 0x93, 0xeb, 0xd5, 0x36, 0xe0, 0xe4, 0x60, - 0x8c, 0xb0, 0x2b, 0xad, 0x0c, 0xeb, 0x4a, 0xa7, 0x12, 0x5d, 0xe9, 0xe8, 0xf9, 0x4d, 0xc7, 0xcf, - 0xaf, 0xfa, 0xc5, 0x14, 0x10, 0x5c, 0xf7, 0xa8, 0x97, 0xb7, 0xe0, 0x8e, 0x96, 0x1e, 0x72, 0x47, - 0xcb, 0xc4, 0x6f, 0x1d, 0x2b, 0xfd, 0x77, 0xb4, 0x11, 0x2a, 0xea, 0xc9, 0x0b, 0xdc, 0xed, 0x01, - 0x17, 0xb8, 0x11, 0x8a, 0x68, 0xc9, 0xdb, 0x9d, 0xfa, 0x04, 0x6a, 0xfd, 0x5a, 0x70, 0xc3, 0xcc, - 0x23, 0x71, 0xcb, 0x38, 0xd3, 0xb7, 0xcf, 0x43, 0x2e, 0x2d, 0x5f, 0x48, 0xc1, 0x69, 0x9c, 0x4f, - 0x66, 0x6a, 0x63, 0xd5, 0x74, 0x9f, 0x24, 0xcc, 0xec, 0x66, 0xdf, 0xe7, 0x87, 0x2c, 0x5f, 0x4f, - 0xc2, 0xe3, 0x46, 0xc6, 0xe0, 0xc4, 0x40, 0x84, 0xc9, 0xda, 0xd8, 0xd2, 0xdb, 0x30, 0xa7, 0xdb, - 0x9d, 0xfa, 0x53, 0xe6, 0xd8, 0xa6, 0xde, 0xa6, 0xeb, 0x6e, 0x84, 0xfd, 0xa5, 0xe2, 0x2a, 0xfe, - 0x5e, 0xec, 0x9a, 0x0f, 0x95, 0xff, 0x4a, 0xd3, 0xae, 0xf9, 0x83, 0x54, 0x66, 0xf5, 0xee, 0xc3, - 0xa5, 0x1f, 0xa5, 0x72, 0x62, 0x66, 0x3d, 0x87, 0x3b, 0x78, 0xe5, 0x1f, 0x01, 0x00, 0x00, 0xff, - 0xff, 0x7f, 0x43, 0xaa, 0xd0, 0x7b, 0x3b, 0x00, 0x00, + 0x82, 0x1c, 0x02, 0x3b, 0x01, 0x82, 0xe4, 0x90, 0x4b, 0x90, 0xbb, 0x13, 0xe4, 0x16, 0xc4, 0x41, + 0x6e, 0x49, 0x90, 0x53, 0xce, 0x39, 0xe6, 0x12, 0x74, 0x75, 0xcf, 0x97, 0xe4, 0x2e, 0xb9, 0x4b, + 0xd9, 0x46, 0x2e, 0x0b, 0x76, 0x75, 0x55, 0x4f, 0x55, 0x75, 0x75, 0x55, 0x75, 0x55, 0x2f, 0x94, + 0x69, 0xd7, 0xbc, 0x4c, 0xbb, 0x66, 0xbd, 0xeb, 0xd8, 0x9e, 0x4d, 0xc0, 0xa2, 0x5b, 0xb4, 0x43, + 0xeb, 0xb4, 0x6b, 0xd6, 0xce, 0x6e, 0xda, 0xf6, 0x66, 0x9b, 0x5d, 0xc6, 0x99, 0xf5, 0xde, 0xc6, + 0x65, 0xcf, 0xec, 0x30, 0xd7, 0xa3, 0x9d, 0xae, 0x40, 0xae, 0x9d, 0x49, 0x22, 0xec, 0x38, 0xb4, + 0xdb, 0x65, 0x8e, 0x2b, 0xe6, 0xd5, 0x1f, 0xa7, 0x20, 0xbf, 0xa8, 0xeb, 0x76, 0xcf, 0xf2, 0xc8, + 0xf3, 0x90, 0xe9, 0xb9, 0xcc, 0xa9, 0x2a, 0xe7, 0x94, 0x4b, 0xa5, 0x85, 0x4a, 0x3d, 0xfc, 0x4e, + 0xfd, 0xb1, 0xcb, 0x1c, 0x0d, 0x67, 0xc9, 0x49, 0xc8, 0xed, 0xd0, 0x76, 0x9b, 0x79, 0xd5, 0xd4, + 0x39, 0xe5, 0x52, 0x51, 0x93, 0x23, 0x72, 0x1c, 0xb2, 0xac, 0x43, 0xcd, 0x76, 0x35, 0x8d, 0x60, + 0x31, 0x20, 0x57, 0x20, 0x6f, 0xb0, 0x6d, 0x53, 0x67, 0x6e, 0x35, 0x73, 0x2e, 0x7d, 0xa9, 0xb4, + 0x30, 0x17, 0x5d, 0x56, 0x7e, 0x79, 0x05, 0x31, 0x34, 0x1f, 0x93, 0x9c, 0x82, 0xa2, 0xde, 0x73, + 0x3d, 0xbb, 0xd3, 0x32, 0x8d, 0x6a, 0x16, 0x97, 0x2b, 0x08, 0x40, 0xd3, 0x20, 0x37, 0xa0, 0xb4, + 0xcd, 0x1c, 0x73, 0x63, 0xaf, 0xc5, 0x65, 0xad, 0xe6, 0x90, 0xd9, 0x5a, 0x5d, 0xc8, 0x59, 0xf7, + 0xe5, 0xac, 0x3f, 0xf2, 0x15, 0xa1, 0x81, 0x40, 0xe7, 0x00, 0xf2, 0x36, 0x4c, 0x19, 0xa6, 0x4b, + 0xd7, 0xdb, 0x4c, 0x50, 0xe7, 0x0f, 0xa4, 0x2e, 0x49, 0x7c, 0x0e, 0x51, 0xbf, 0xa6, 0x40, 0x59, + 0xf2, 0xbc, 0x8c, 0xfc, 0x90, 0x69, 0x48, 0x99, 0x06, 0x6a, 0xac, 0xa8, 0xa5, 0x4c, 0x83, 0x5c, + 0x83, 0xcc, 0x36, 0x75, 0xdc, 0x6a, 0x0a, 0x85, 0xbd, 0x30, 0x40, 0x58, 0x41, 0x58, 0x7f, 0x42, + 0x1d, 0xb7, 0x61, 0x79, 0xce, 0x9e, 0x86, 0x04, 0xb5, 0x6b, 0x50, 0x0c, 0x40, 0xa4, 0x02, 0xe9, + 0x2d, 0xb6, 0x27, 0x97, 0xe5, 0x3f, 0xb9, 0x76, 0xb7, 0x69, 0xbb, 0xc7, 0xa4, 0xd2, 0xc5, 0xe0, + 0xad, 0xd4, 0x75, 0x25, 0xca, 0x93, 0xd0, 0xe3, 0x21, 0x78, 0x12, 0x84, 0x93, 0xe3, 0xe9, 0x67, + 0x0a, 0x4c, 0xc9, 0xa5, 0x1b, 0x68, 0x06, 0x81, 0x71, 0x28, 0x51, 0xe3, 0xa8, 0x41, 0xa1, 0x4b, + 0x5d, 0x77, 0xc7, 0x76, 0x0c, 0xb9, 0x46, 0x30, 0x26, 0x57, 0x25, 0xd3, 0x69, 0x64, 0x5a, 0x1d, + 0xc0, 0x34, 0xae, 0x3c, 0x39, 0x9e, 0xbf, 0xa3, 0xc0, 0x8c, 0x5c, 0xf9, 0x36, 0xd5, 0xd9, 0xba, + 0x6d, 0x6f, 0x71, 0x6c, 0xcf, 0xde, 0x62, 0x96, 0xcf, 0x36, 0x0e, 0xc8, 0x9b, 0x31, 0x7d, 0xbe, + 0x30, 0x80, 0x35, 0x7f, 0x81, 0xc9, 0x71, 0xf7, 0x6b, 0x05, 0x6a, 0x89, 0xc5, 0x9b, 0x96, 0xeb, + 0x51, 0xcb, 0xbb, 0x43, 0x3b, 0x8c, 0xbc, 0x02, 0xc4, 0x35, 0x37, 0x2d, 0x66, 0xb4, 0xba, 0x6d, + 0xba, 0xc7, 0x9c, 0x96, 0x69, 0x6d, 0xd8, 0x72, 0xe5, 0x8a, 0x98, 0x79, 0x88, 0x13, 0x4d, 0x6b, + 0xc3, 0x26, 0x2b, 0x31, 0x01, 0x5e, 0xdd, 0x47, 0x80, 0xc8, 0x37, 0x26, 0x27, 0xcb, 0xaf, 0x52, + 0x30, 0x2b, 0xbf, 0xc3, 0x17, 0x5e, 0x66, 0x96, 0xc7, 0x1c, 0x7e, 0xe8, 0x7d, 0xde, 0x7d, 0xe3, + 0x2d, 0x08, 0x40, 0xd3, 0xe0, 0x93, 0xeb, 0x3d, 0xcb, 0x68, 0x33, 0x3e, 0x29, 0x4d, 0x45, 0x00, + 0x9a, 0x06, 0x79, 0x19, 0x66, 0x03, 0xb7, 0xd7, 0x72, 0x99, 0x6e, 0x5b, 0x86, 0x8b, 0x5e, 0x28, + 0xad, 0x55, 0x82, 0x89, 0x35, 0x01, 0x27, 0x04, 0x32, 0x2e, 0x6d, 0x7b, 0xd5, 0x0c, 0x2e, 0x82, + 0xbf, 0xc9, 0x69, 0x28, 0x72, 0x1d, 0x51, 0xaf, 0xe7, 0x30, 0xe9, 0x6f, 0x42, 0x00, 0x79, 0x1e, + 0xa6, 0xbb, 0xbd, 0xf5, 0xb6, 0xa9, 0xb7, 0xb6, 0xd8, 0x5e, 0xab, 0xe7, 0xb4, 0xd1, 0xe7, 0x14, + 0xb5, 0x29, 0x01, 0xbd, 0xcb, 0xf6, 0x1e, 0x3b, 0x6d, 0x72, 0x43, 0xea, 0x34, 0x8f, 0x3a, 0xbd, + 0x38, 0x40, 0xa7, 0xa1, 0xac, 0x93, 0x53, 0xe5, 0xb7, 0xc2, 0xc3, 0x7f, 0x07, 0x5d, 0xd8, 0x10, + 0x93, 0x3d, 0xd8, 0x05, 0x08, 0xf2, 0xc9, 0x71, 0xf6, 0x8d, 0xd0, 0x05, 0xac, 0x79, 0x8c, 0x76, + 0x86, 0x30, 0x76, 0x35, 0xc6, 0xd8, 0xa0, 0x63, 0x8e, 0xd4, 0x93, 0xe3, 0x6b, 0x19, 0x66, 0x17, + 0x0d, 0xe3, 0xb6, 0x63, 0x32, 0xcb, 0x70, 0x35, 0xf6, 0x7e, 0x8f, 0xb9, 0x1e, 0x5f, 0xc0, 0x34, + 0xdc, 0xaa, 0x72, 0x2e, 0xcd, 0x17, 0x30, 0x0d, 0x97, 0x9b, 0x04, 0x8f, 0x76, 0x16, 0xed, 0x30, + 0xc1, 0x5c, 0x51, 0x0b, 0x01, 0xea, 0x3d, 0x38, 0xbe, 0x68, 0x18, 0x77, 0x1c, 0xbb, 0xd7, 0xe5, + 0x91, 0x31, 0x58, 0x67, 0x0e, 0x0a, 0x9b, 0x1c, 0x18, 0x9a, 0x70, 0x1e, 0xc7, 0x4d, 0x83, 0x4f, + 0x71, 0xfa, 0x16, 0xff, 0x8e, 0x58, 0x2f, 0xcf, 0xc7, 0x4d, 0xc3, 0x55, 0xbf, 0xaf, 0xc0, 0xdc, + 0x62, 0xcf, 0x7b, 0xca, 0x2c, 0xcf, 0xd4, 0xa9, 0xc7, 0x44, 0x84, 0xf0, 0xd7, 0xbc, 0x02, 0x79, + 0x2a, 0x34, 0x21, 0x03, 0xf3, 0xdc, 0xd0, 0xa0, 0xa2, 0xf9, 0x98, 0x64, 0x01, 0x72, 0xba, 0xc3, + 0xa8, 0x27, 0x14, 0x30, 0x28, 0xc2, 0x2d, 0xd9, 0x76, 0xfb, 0x09, 0xd7, 0x8a, 0x26, 0x31, 0xb9, + 0x37, 0xf6, 0x25, 0x94, 0x31, 0x3c, 0x18, 0xf7, 0xb1, 0x28, 0x23, 0xf6, 0x38, 0x2c, 0xfa, 0x41, + 0xfe, 0x59, 0xb1, 0xf8, 0x5d, 0x05, 0xaa, 0x51, 0x16, 0x31, 0x3c, 0xf8, 0x1c, 0x2e, 0x24, 0x39, + 0xac, 0x0e, 0x0b, 0x28, 0xcf, 0x8e, 0xc1, 0xdf, 0x29, 0x70, 0x2a, 0xca, 0xa0, 0xef, 0x63, 0x7d, + 0x1e, 0xdf, 0x48, 0xf2, 0x78, 0x6a, 0x1f, 0xc7, 0xfc, 0xcc, 0xd8, 0x24, 0x75, 0xc8, 0xb8, 0x7b, + 0x96, 0x8e, 0x0e, 0x72, 0xff, 0xd5, 0x10, 0x4f, 0xfd, 0x85, 0x02, 0x2f, 0x0e, 0x12, 0x2b, 0x12, + 0x3a, 0x7c, 0x09, 0x6f, 0x25, 0x25, 0x7c, 0x71, 0xb4, 0xd0, 0xf3, 0xec, 0xf6, 0xe4, 0x43, 0x05, + 0xe6, 0xa3, 0xcc, 0x87, 0x3e, 0xda, 0xe7, 0xf9, 0x5a, 0x92, 0xe7, 0xf9, 0x7d, 0x5d, 0xfb, 0xa7, + 0x77, 0x04, 0x85, 0xc3, 0x1e, 0xeb, 0x08, 0x4a, 0x92, 0x4f, 0xed, 0x08, 0xa2, 0xeb, 0x1e, 0xeb, + 0x08, 0x0a, 0x8a, 0x67, 0xc6, 0xe0, 0x3d, 0x38, 0xbe, 0x44, 0xad, 0x49, 0xf9, 0xed, 0x06, 0x1c, + 0x5b, 0x6a, 0xdb, 0xfa, 0xd6, 0x11, 0x83, 0xc9, 0xff, 0x67, 0x60, 0x7a, 0xf9, 0x29, 0xb5, 0x2c, + 0xd6, 0xbe, 0xcf, 0x5c, 0x97, 0x6e, 0x32, 0x32, 0x0f, 0xa0, 0x0b, 0x48, 0xc8, 0x51, 0x51, 0x42, + 0x9a, 0x06, 0x9f, 0xee, 0x08, 0xcc, 0x30, 0x1d, 0x2a, 0x4a, 0x48, 0xd3, 0x20, 0x97, 0x21, 0xa3, + 0xdb, 0x86, 0x90, 0x9e, 0x7b, 0x91, 0xa4, 0xce, 0x9a, 0x96, 0x77, 0x65, 0x41, 0x1e, 0x61, 0x8e, + 0xc8, 0xb3, 0x2b, 0x97, 0x59, 0x86, 0x48, 0xbd, 0x44, 0x62, 0x54, 0x10, 0x80, 0xa6, 0x11, 0xd3, + 0x67, 0x36, 0xe1, 0x2b, 0xaa, 0x90, 0xd7, 0x6d, 0xcb, 0x63, 0x96, 0x27, 0x73, 0x22, 0x7f, 0xc8, + 0x6f, 0x69, 0x62, 0x3f, 0x46, 0xbd, 0x67, 0x81, 0x40, 0xc7, 0x5b, 0xda, 0x0d, 0x28, 0xf5, 0xba, + 0x46, 0x40, 0x5c, 0x38, 0x98, 0x58, 0xa0, 0x23, 0xf1, 0x5b, 0x00, 0xfc, 0x7e, 0x6b, 0xba, 0xc8, + 0x56, 0xf1, 0x40, 0xbb, 0x89, 0x60, 0x73, 0x45, 0x38, 0xb6, 0xdd, 0x69, 0xa1, 0xb0, 0x20, 0x84, + 0xe5, 0x80, 0x55, 0x2e, 0x6c, 0xd4, 0x48, 0x4a, 0x71, 0x23, 0x39, 0x03, 0x25, 0x69, 0x24, 0x2d, + 0xdb, 0x62, 0xd5, 0x29, 0xb1, 0x23, 0xc2, 0x4e, 0x1e, 0x58, 0x2c, 0x3a, 0xef, 0xed, 0xd8, 0xd5, + 0x72, 0x74, 0xfe, 0xd1, 0x8e, 0xad, 0x7e, 0x45, 0x01, 0x12, 0x37, 0x81, 0x7b, 0xa6, 0xeb, 0x91, + 0xab, 0x50, 0x90, 0xbb, 0x2a, 0xcc, 0x89, 0x0b, 0x12, 0x39, 0x33, 0x71, 0x0a, 0x2d, 0xc0, 0x25, + 0x67, 0xa1, 0x64, 0xb1, 0x5d, 0xaf, 0xa5, 0xf7, 0x1c, 0xd7, 0x76, 0xa4, 0x81, 0x00, 0x07, 0x2d, + 0x23, 0x84, 0x23, 0x74, 0x1d, 0xb6, 0xed, 0x23, 0x88, 0x63, 0x02, 0x1c, 0x24, 0x10, 0xd4, 0x9f, + 0x73, 0x86, 0x70, 0x43, 0xf0, 0xb0, 0xf8, 0xa6, 0x4d, 0x20, 0x83, 0xaa, 0x11, 0x16, 0x89, 0xbf, + 0xc9, 0x39, 0x28, 0x19, 0xcc, 0xd5, 0x1d, 0xb3, 0xeb, 0x99, 0xb6, 0x25, 0x3f, 0x16, 0x05, 0x71, + 0xc5, 0xb5, 0xa9, 0xb5, 0xd9, 0xf2, 0xe8, 0xa6, 0xfc, 0x54, 0x9e, 0x8f, 0x1f, 0xd1, 0x4d, 0x6e, + 0xc9, 0x74, 0x9b, 0x7a, 0xd4, 0xc1, 0xbc, 0x5a, 0x98, 0x5e, 0x51, 0x40, 0x78, 0x52, 0x4d, 0x20, + 0x63, 0x77, 0x99, 0x85, 0x76, 0x57, 0xd0, 0xf0, 0x37, 0xdf, 0xa3, 0x0e, 0xdd, 0x6d, 0x09, 0x4f, + 0xc2, 0xad, 0x2e, 0xab, 0x15, 0x3a, 0x74, 0x77, 0x99, 0x8f, 0xd5, 0xdb, 0x70, 0x7c, 0x85, 0xb5, + 0x99, 0xc7, 0x8e, 0x78, 0x26, 0x2f, 0x03, 0x11, 0xeb, 0xc4, 0xc4, 0x1f, 0xee, 0x26, 0xd4, 0x3b, + 0x70, 0x46, 0x10, 0xdc, 0x63, 0xd4, 0x60, 0xce, 0xba, 0x4d, 0x1d, 0x43, 0x63, 0xba, 0xcd, 0xff, + 0x0a, 0xe2, 0x17, 0x60, 0xba, 0x1d, 0xce, 0x85, 0x4b, 0x94, 0x23, 0xd0, 0xa6, 0xa1, 0xd6, 0xa1, + 0x26, 0x16, 0x5a, 0xb5, 0x3d, 0x73, 0x83, 0xbb, 0x51, 0xd3, 0xb6, 0x86, 0xcb, 0xa1, 0xea, 0x70, + 0x42, 0xe0, 0xaf, 0x79, 0xb6, 0x43, 0x37, 0xd9, 0x83, 0xf5, 0xff, 0x61, 0xba, 0x87, 0x36, 0x09, + 0xba, 0xdd, 0x6e, 0x33, 0x1d, 0xb7, 0x45, 0x7c, 0x2b, 0x02, 0xf1, 0x93, 0xe6, 0x54, 0x98, 0x34, + 0x57, 0x21, 0xbf, 0xcd, 0xcf, 0x82, 0x6d, 0xf9, 0xdb, 0x24, 0x87, 0x6a, 0x0b, 0x4e, 0x0d, 0xf8, + 0x88, 0x1b, 0xc6, 0x75, 0xb0, 0x11, 0xd2, 0xf2, 0x99, 0x2b, 0x2d, 0x9c, 0x8f, 0x5a, 0xea, 0x40, + 0x0e, 0xb5, 0xa2, 0x2d, 0x7f, 0xb9, 0xea, 0xdf, 0x14, 0xc8, 0x36, 0xb6, 0xf9, 0x11, 0x1c, 0x64, + 0x62, 0x8b, 0x00, 0x5d, 0xc7, 0xee, 0x32, 0xc7, 0x33, 0x99, 0x7f, 0x55, 0x88, 0xad, 0x8f, 0xa4, + 0xf5, 0x87, 0x01, 0x8e, 0xb8, 0x29, 0x44, 0x88, 0xc8, 0x75, 0x28, 0x06, 0x57, 0x41, 0xe9, 0x18, + 0xf7, 0x73, 0x28, 0x21, 0x32, 0xf7, 0x7f, 0x6c, 0xd7, 0xe3, 0x86, 0x21, 0x0c, 0xb4, 0xa0, 0x05, + 0xe3, 0xda, 0xdb, 0x30, 0x93, 0xf8, 0xe8, 0x58, 0x77, 0x91, 0x8f, 0x14, 0xc8, 0x09, 0x43, 0x1d, + 0xb1, 0xf6, 0xf6, 0x1a, 0x64, 0x5d, 0x2f, 0x0c, 0x87, 0xfb, 0xba, 0x76, 0x81, 0xa9, 0xde, 0x86, + 0xec, 0x1a, 0xff, 0x41, 0x00, 0x72, 0xb7, 0xb5, 0x66, 0x63, 0x75, 0xa5, 0xf2, 0x1c, 0x99, 0x81, + 0x52, 0x73, 0xf5, 0x49, 0xf3, 0x51, 0xa3, 0xb5, 0xd6, 0x58, 0x7d, 0x54, 0x51, 0xc8, 0x31, 0x98, + 0x91, 0x00, 0xad, 0xb1, 0xdc, 0x68, 0x3e, 0x69, 0xac, 0x54, 0x52, 0xa4, 0x04, 0xf9, 0xa5, 0x7b, + 0x0f, 0x96, 0xef, 0x36, 0x56, 0x2a, 0x69, 0x55, 0x03, 0x10, 0xac, 0xa2, 0x67, 0x7a, 0x05, 0xf2, + 0x1b, 0xe2, 0x84, 0xc9, 0xed, 0x26, 0x51, 0x8e, 0x05, 0xa2, 0xe6, 0xa3, 0x90, 0x93, 0x90, 0x8b, + 0xb9, 0x22, 0x39, 0x52, 0x0d, 0x98, 0xb9, 0xc3, 0xbc, 0x58, 0x24, 0x1e, 0xf3, 0xa0, 0x92, 0xf3, + 0x30, 0xb5, 0x21, 0x13, 0x46, 0x34, 0xbe, 0x34, 0x22, 0x94, 0x7c, 0x18, 0xb7, 0xad, 0x0f, 0xd3, + 0x90, 0xc5, 0x63, 0xdc, 0x57, 0x18, 0xe3, 0x61, 0x96, 0x3b, 0x39, 0xdb, 0x89, 0xc4, 0x51, 0x09, + 0x69, 0x1a, 0x81, 0x29, 0xa6, 0x87, 0x7b, 0xbb, 0xcc, 0xfe, 0xde, 0x2e, 0x1b, 0xf7, 0x76, 0x35, + 0xee, 0xcf, 0x3d, 0x6a, 0x50, 0x8f, 0xca, 0x78, 0x19, 0x8c, 0x13, 0x9e, 0x30, 0x9f, 0xf4, 0x84, + 0x75, 0xe9, 0x09, 0x0b, 0x07, 0x67, 0xe5, 0xe8, 0x25, 0xe7, 0x01, 0x98, 0xb1, 0xc9, 0xa4, 0x9b, + 0x2c, 0xa2, 0x9b, 0x2c, 0x72, 0x08, 0xfa, 0xc9, 0xb8, 0x13, 0x85, 0xb8, 0x13, 0x4d, 0xc6, 0xee, + 0xd2, 0x51, 0x62, 0xf7, 0xd4, 0x38, 0xb1, 0x5b, 0x5d, 0x85, 0x22, 0xee, 0x14, 0xda, 0xd8, 0x4b, + 0x90, 0x43, 0xef, 0xea, 0x9b, 0xd8, 0x6c, 0xd4, 0xc4, 0x84, 0x5f, 0x96, 0x08, 0x43, 0x0d, 0xec, + 0x9b, 0x29, 0x28, 0x07, 0xd9, 0x1e, 0x2e, 0xba, 0x02, 0x25, 0xe1, 0xc2, 0xb9, 0x09, 0xf9, 0x2b, + 0x5f, 0xe8, 0x5b, 0xd9, 0xc7, 0x0f, 0x47, 0x1a, 0x6c, 0x06, 0x69, 0xe3, 0xb0, 0xef, 0xd5, 0x7e, + 0xa8, 0x48, 0x01, 0x38, 0xda, 0xb3, 0x3b, 0xd3, 0xb7, 0xfc, 0x33, 0x3d, 0x0d, 0xb0, 0xf6, 0xf8, + 0x61, 0x43, 0x5b, 0x5c, 0xb9, 0xdf, 0x5c, 0xad, 0x3c, 0x47, 0x8a, 0x90, 0x15, 0x3f, 0x15, 0x7e, + 0xdc, 0xef, 0x37, 0xee, 0x2f, 0x35, 0xb4, 0x4a, 0x8a, 0x54, 0x60, 0xea, 0xdd, 0x07, 0xcd, 0xd5, + 0x96, 0xd6, 0xf8, 0xf7, 0xc7, 0x8d, 0xb5, 0x47, 0x95, 0xb4, 0xfa, 0x25, 0x05, 0x4e, 0x37, 0x3b, + 0x5d, 0xdb, 0x09, 0xae, 0x5b, 0x89, 0x80, 0x79, 0xc8, 0xcb, 0xe8, 0xab, 0x90, 0x75, 0x98, 0x2b, + 0x7b, 0x03, 0xfb, 0xdb, 0xa9, 0x40, 0x54, 0xff, 0x15, 0x2a, 0xef, 0xda, 0xa6, 0x35, 0x6a, 0x9c, + 0xfd, 0x37, 0x38, 0xc1, 0xd1, 0x1f, 0xd9, 0x3d, 0x74, 0x00, 0x96, 0xe7, 0xd3, 0x5c, 0x80, 0xb2, + 0x17, 0x00, 0x43, 0xc2, 0xa9, 0x10, 0xd8, 0x34, 0xd4, 0xfb, 0x70, 0xe2, 0xae, 0xa9, 0x6f, 0x4d, + 0xea, 0x02, 0xf0, 0xd7, 0x34, 0xcc, 0xf6, 0xc5, 0xfb, 0x11, 0x03, 0x3d, 0x5f, 0xd7, 0xde, 0xb1, + 0x58, 0xc4, 0xf5, 0xe4, 0x71, 0xdc, 0x34, 0xc8, 0xf5, 0xc4, 0x15, 0xa6, 0xb4, 0x70, 0xba, 0x4f, + 0x91, 0x6b, 0x9e, 0x63, 0x5a, 0x9b, 0x42, 0x95, 0x61, 0x42, 0x7e, 0x1c, 0xb2, 0xae, 0x6e, 0x3b, + 0x0c, 0x1d, 0x53, 0x5a, 0x13, 0x03, 0xee, 0x77, 0xdc, 0xde, 0xba, 0x98, 0xc8, 0xe2, 0x44, 0x30, + 0xe6, 0x9e, 0xc0, 0xea, 0x75, 0x5a, 0x62, 0x52, 0xa6, 0x53, 0x56, 0xaf, 0xb3, 0xe6, 0x13, 0x06, + 0x0e, 0x2b, 0x9f, 0x70, 0x58, 0x09, 0x2f, 0x51, 0x38, 0x8a, 0x97, 0x28, 0x8e, 0x95, 0xe1, 0xdf, + 0x80, 0x12, 0xdb, 0xed, 0x9a, 0x8e, 0xec, 0x00, 0xc1, 0xc1, 0xc4, 0x02, 0x1d, 0x89, 0x09, 0x64, + 0x1c, 0x6a, 0x6d, 0xa1, 0x57, 0x4b, 0x6b, 0xf8, 0x9b, 0xa8, 0x50, 0xe6, 0xde, 0x30, 0xd4, 0x03, + 0xf7, 0x5a, 0x65, 0xad, 0xd4, 0xa1, 0xbb, 0xab, 0x52, 0x15, 0xea, 0xef, 0x15, 0x38, 0xd1, 0xb7, + 0xd7, 0xe8, 0x52, 0xae, 0x41, 0xde, 0xc1, 0x91, 0xef, 0x4e, 0x62, 0x15, 0x82, 0xfe, 0x7c, 0xd0, + 0xc7, 0x26, 0x4b, 0x50, 0x16, 0x16, 0xe0, 0x93, 0xa7, 0x46, 0x21, 0x9f, 0x42, 0x1a, 0x4d, 0xae, + 0x91, 0x48, 0xf5, 0xd3, 0x07, 0xa5, 0xfa, 0x99, 0xbe, 0x54, 0xbf, 0x8e, 0x36, 0xbc, 0x3d, 0x72, + 0xa6, 0xfb, 0xbf, 0x70, 0xec, 0x9e, 0x69, 0x6d, 0x4d, 0xa8, 0x7a, 0x35, 0x6e, 0xb5, 0xe9, 0x97, + 0x0a, 0xd4, 0xb8, 0xd6, 0xe3, 0x77, 0x9f, 0xe0, 0x1c, 0x1f, 0x70, 0x71, 0x7e, 0x0d, 0xb2, 0x6d, + 0xb3, 0x63, 0x7a, 0x23, 0xf9, 0x5a, 0xc4, 0x24, 0xaf, 0x43, 0x7e, 0xc3, 0x76, 0x76, 0xa8, 0x63, + 0x0c, 0x4d, 0x1b, 0x43, 0x1e, 0x7d, 0xd4, 0x48, 0x80, 0xc8, 0xc4, 0x02, 0xd2, 0xd7, 0x15, 0x20, + 0x9c, 0xfd, 0x84, 0xb7, 0x0d, 0xf8, 0x52, 0x46, 0xe6, 0x6b, 0xfc, 0xb0, 0x11, 0x61, 0x2a, 0x1d, + 0x63, 0xca, 0x81, 0x59, 0xce, 0x13, 0x1a, 0x80, 0xbb, 0xdf, 0x55, 0x6f, 0x48, 0xd8, 0x0b, 0xd9, + 0x4f, 0x8f, 0xca, 0xbe, 0xfa, 0x53, 0x7e, 0x9c, 0xfc, 0x8f, 0x8e, 0xea, 0x8a, 0x0f, 0xb1, 0x7d, + 0x81, 0x9a, 0xd2, 0x87, 0x50, 0x53, 0x7c, 0xef, 0x3e, 0x51, 0xe0, 0x12, 0x67, 0xb9, 0xef, 0x48, + 0xba, 0x8b, 0x8e, 0xdd, 0xb3, 0x8c, 0x07, 0xe2, 0x5c, 0x8e, 0x73, 0xdb, 0x23, 0x0b, 0x71, 0x89, + 0xfa, 0xdd, 0xfc, 0xe3, 0x7e, 0x91, 0xa2, 0x81, 0x23, 0x1d, 0x0f, 0x1c, 0x57, 0x20, 0x27, 0x5c, + 0x9d, 0x3c, 0x4f, 0x03, 0xc5, 0xbd, 0xfa, 0xba, 0x2c, 0x98, 0x09, 0x54, 0xf5, 0x2f, 0x0a, 0xcc, + 0x0f, 0x96, 0x6b, 0x4c, 0x61, 0x4e, 0x41, 0xd1, 0x67, 0xcc, 0x0f, 0x95, 0x05, 0xc9, 0x99, 0x7b, + 0x08, 0x1b, 0x19, 0xb6, 0x11, 0x11, 0x29, 0xb3, 0xa3, 0x4b, 0xf9, 0xe7, 0x94, 0x38, 0x79, 0xf7, + 0xa9, 0xa7, 0x3f, 0x65, 0x47, 0x39, 0x79, 0xb7, 0xa0, 0x4c, 0x7b, 0xde, 0x53, 0xdb, 0x31, 0x3d, + 0xea, 0x99, 0xdb, 0xa3, 0xd4, 0x26, 0xe3, 0x04, 0xb8, 0xeb, 0x74, 0x9d, 0xb5, 0x47, 0x0a, 0xee, + 0x02, 0x15, 0x6b, 0x41, 0xa6, 0xd5, 0x72, 0xcd, 0x0f, 0xd8, 0x7e, 0x9b, 0xeb, 0xf3, 0x9a, 0xef, + 0x98, 0xd6, 0x9a, 0xf9, 0x01, 0x43, 0x3a, 0xba, 0x2b, 0xe8, 0xb2, 0xa3, 0xd0, 0xd1, 0x5d, 0xa4, + 0x5b, 0x80, 0xec, 0xfb, 0x3d, 0xe6, 0xec, 0xc9, 0x07, 0x16, 0x07, 0xf0, 0x88, 0xa8, 0xea, 0x2e, + 0x54, 0xb9, 0x8a, 0x07, 0x56, 0x2e, 0x0e, 0xa1, 0xe8, 0x97, 0xa0, 0xa2, 0x53, 0xfd, 0x29, 0xc3, + 0xe7, 0x1a, 0x31, 0xc7, 0x33, 0x13, 0xc0, 0x65, 0x10, 0xfb, 0x9e, 0x02, 0x73, 0xfc, 0xd3, 0x83, + 0xeb, 0x13, 0xff, 0x02, 0x79, 0x99, 0xc2, 0x49, 0xc3, 0xcd, 0x89, 0x0c, 0x2e, 0x51, 0x23, 0x49, + 0xf5, 0xd5, 0x48, 0x26, 0x67, 0xb4, 0xea, 0x6f, 0x15, 0xb8, 0xc8, 0x39, 0x8c, 0x66, 0xae, 0xc3, + 0x9c, 0xc7, 0x28, 0xb9, 0xec, 0xe7, 0xc2, 0x75, 0xfc, 0x49, 0x81, 0xd3, 0x03, 0x85, 0x1a, 0x4b, + 0x92, 0xcf, 0xb5, 0xdf, 0xf8, 0x63, 0x0a, 0x4e, 0xc6, 0x45, 0x0c, 0x84, 0x5b, 0x86, 0x69, 0x9d, + 0x7a, 0x6c, 0xd3, 0x76, 0xf6, 0x5a, 0xae, 0x47, 0x1d, 0xdf, 0xb6, 0xf7, 0xdf, 0x8a, 0xb2, 0x4f, + 0xb3, 0xc6, 0x49, 0xc8, 0x3b, 0x30, 0x15, 0x2c, 0xc2, 0x2c, 0x63, 0xa4, 0xdd, 0x2c, 0xf9, 0x14, + 0x0d, 0xcb, 0x20, 0x37, 0x00, 0xf0, 0xe3, 0x22, 0x19, 0x4e, 0x8f, 0x40, 0x5e, 0x44, 0x7c, 0xcc, + 0x86, 0xaf, 0x41, 0x81, 0x59, 0x86, 0x20, 0xcd, 0x8c, 0x40, 0x9a, 0x67, 0x96, 0x81, 0x84, 0xc1, + 0xb6, 0xe4, 0x0e, 0xb1, 0x2d, 0x85, 0xd8, 0xc9, 0xf8, 0x89, 0x4c, 0x05, 0x78, 0x16, 0x10, 0xcf, + 0x41, 0x86, 0x9e, 0xdb, 0xcf, 0x36, 0x11, 0xf8, 0xaa, 0x02, 0x59, 0x0c, 0x23, 0xfc, 0x3c, 0x75, + 0xf8, 0x8f, 0x48, 0xae, 0x82, 0xe3, 0xa6, 0x41, 0x9e, 0x1f, 0x14, 0x25, 0x0a, 0x93, 0x88, 0x04, + 0x04, 0x32, 0x41, 0x14, 0xc8, 0x6a, 0xf8, 0x5b, 0xbd, 0x0e, 0x45, 0xe4, 0x08, 0x2f, 0x24, 0x2f, + 0x83, 0xe0, 0x82, 0x0d, 0xac, 0x9c, 0x20, 0x9e, 0xe6, 0x63, 0xf0, 0x23, 0x3c, 0x15, 0x75, 0xd8, + 0x7d, 0x45, 0xb2, 0x2a, 0xe4, 0xdd, 0x1e, 0xfa, 0x53, 0xff, 0x9a, 0x2a, 0x87, 0xd1, 0xee, 0x4f, + 0x3a, 0xde, 0xfd, 0x21, 0xb2, 0x03, 0x25, 0x59, 0xec, 0x6f, 0x32, 0x65, 0x13, 0x4d, 0xa6, 0xc4, + 0x65, 0x32, 0x37, 0xd6, 0x65, 0xf2, 0x4c, 0xac, 0xe3, 0x93, 0x47, 0x3d, 0x47, 0x20, 0xea, 0xff, + 0x41, 0x25, 0x2a, 0x21, 0xea, 0xe8, 0x26, 0x94, 0xad, 0x68, 0x98, 0x92, 0x9a, 0x8a, 0xf5, 0x24, + 0xa3, 0x44, 0x5a, 0x1c, 0x7d, 0x9c, 0xd8, 0xf4, 0x10, 0xaa, 0x0f, 0x1d, 0xbb, 0x63, 0xcb, 0x66, + 0xc2, 0x04, 0xea, 0x0e, 0xef, 0xc1, 0x31, 0x8d, 0x51, 0xe3, 0xe8, 0x15, 0xff, 0xc8, 0x01, 0x4b, + 0x47, 0x0f, 0x98, 0xfa, 0xdf, 0x30, 0xd7, 0xf7, 0x85, 0x80, 0xe9, 0x9b, 0x03, 0xca, 0xfd, 0x67, + 0xa3, 0x8a, 0x1b, 0xc0, 0x5c, 0xb4, 0xd8, 0xff, 0x2e, 0xa4, 0xb5, 0xae, 0x3e, 0xc8, 0xd0, 0xba, + 0x74, 0xaf, 0x6d, 0xd3, 0xa0, 0x1e, 0x22, 0x87, 0x5c, 0x15, 0x4f, 0x3d, 0xaf, 0xdb, 0xe2, 0xdc, + 0x4b, 0x4b, 0xe3, 0xe3, 0xbb, 0x6c, 0x4f, 0x7d, 0x13, 0xf2, 0x6b, 0xcc, 0x75, 0xb9, 0x78, 0xdc, + 0x1c, 0xd1, 0x28, 0xc4, 0xa2, 0x05, 0xcd, 0x1f, 0x86, 0x4f, 0x8f, 0x52, 0x91, 0xa7, 0x47, 0xea, + 0x1f, 0x52, 0x50, 0x8e, 0x71, 0x39, 0x41, 0x05, 0x86, 0x45, 0xff, 0x4c, 0xa4, 0xe8, 0x1f, 0xed, + 0xb0, 0x64, 0x63, 0x1d, 0x16, 0x72, 0x11, 0x66, 0xba, 0xcc, 0xe9, 0x98, 0x28, 0x4a, 0xcb, 0x61, + 0xd4, 0x90, 0xc5, 0x98, 0xe9, 0x10, 0xcc, 0xd5, 0xca, 0x0d, 0x2f, 0x82, 0xb8, 0xe3, 0x98, 0x9e, + 0xe8, 0xae, 0x66, 0xb5, 0xc8, 0x02, 0xff, 0xc1, 0xc1, 0x9f, 0x5d, 0x85, 0x46, 0xdd, 0x81, 0x4a, + 0x4c, 0xb3, 0x8b, 0xfa, 0xd6, 0x24, 0xfb, 0x51, 0x51, 0xb5, 0x67, 0x62, 0x76, 0xdb, 0x80, 0xd9, + 0xe4, 0x87, 0x5d, 0xf2, 0x2a, 0x64, 0xa8, 0xbe, 0xe5, 0x5b, 0xea, 0xe9, 0xa8, 0xa5, 0x26, 0x91, + 0x35, 0xc4, 0x54, 0x1b, 0x30, 0x1d, 0x37, 0x7d, 0x72, 0x05, 0xf2, 0xc2, 0x80, 0xfd, 0x65, 0xe6, + 0x86, 0x2e, 0xa3, 0xf9, 0x98, 0xea, 0x7b, 0x09, 0x6e, 0xd0, 0xf3, 0x1c, 0x66, 0xa5, 0xa1, 0x05, + 0xee, 0x8f, 0x33, 0x00, 0x61, 0x66, 0xd2, 0x77, 0xa4, 0xb8, 0xe1, 0x9b, 0x5e, 0x3b, 0x68, 0x3d, + 0xe1, 0x20, 0xd9, 0xc3, 0x48, 0xf7, 0xf7, 0x30, 0x6a, 0x50, 0xf0, 0x53, 0x0c, 0x54, 0x70, 0x59, + 0x0b, 0xc6, 0x64, 0x1e, 0xc0, 0xb5, 0x1d, 0xaf, 0x65, 0x3b, 0x06, 0x73, 0xd0, 0x8c, 0xcb, 0x5a, + 0x91, 0x43, 0x1e, 0x70, 0x40, 0x10, 0x9d, 0x72, 0x38, 0x81, 0xbf, 0x45, 0x98, 0x94, 0x77, 0x90, + 0x3c, 0xc2, 0x83, 0x6b, 0x46, 0x5f, 0xe9, 0xad, 0xd0, 0x57, 0x7a, 0xc3, 0xe7, 0xe0, 0xd4, 0x6a, + 0xe1, 0xfb, 0x1a, 0x34, 0xc4, 0x02, 0x67, 0xc7, 0x6a, 0xe0, 0xb3, 0xd1, 0x79, 0x00, 0x9e, 0xc1, + 0x50, 0x1d, 0x83, 0x2c, 0x08, 0x76, 0x98, 0x65, 0x2c, 0x22, 0x80, 0x4f, 0x63, 0x7d, 0x4c, 0x54, + 0xa5, 0x4b, 0x62, 0x9a, 0x43, 0x34, 0x0e, 0x88, 0x15, 0x38, 0xa7, 0xf6, 0x2f, 0x70, 0x96, 0xc7, + 0x3a, 0x3e, 0x6f, 0xc6, 0xb2, 0xb2, 0xe9, 0x83, 0x1b, 0x8e, 0x61, 0x4e, 0xf6, 0x46, 0x24, 0x27, + 0x9b, 0x39, 0x90, 0x30, 0xc8, 0xc8, 0x6a, 0x50, 0x30, 0x7a, 0x0e, 0x86, 0xa7, 0x6a, 0x45, 0xec, + 0x99, 0x3f, 0x26, 0xe7, 0x61, 0x4a, 0x70, 0x23, 0xd5, 0x34, 0x2b, 0x94, 0x8c, 0x30, 0xa1, 0x28, + 0x75, 0x1d, 0xa6, 0x43, 0x43, 0x42, 0x43, 0xbd, 0x0e, 0xa5, 0x30, 0x4d, 0xf7, 0x8d, 0xf5, 0x64, + 0xd4, 0x58, 0x23, 0x69, 0x7f, 0x14, 0x75, 0xa8, 0xb5, 0x7e, 0xa2, 0xc0, 0xf1, 0xe4, 0x55, 0xe1, + 0x9f, 0xa1, 0x84, 0xfa, 0xf7, 0x14, 0x1c, 0x7f, 0x8c, 0xde, 0x4f, 0xd6, 0x39, 0xfd, 0x48, 0x19, + 0x2d, 0xe4, 0x2b, 0x63, 0x15, 0xf2, 0xdf, 0xc1, 0x7f, 0x54, 0xe8, 0xb6, 0xe9, 0x9e, 0x78, 0x8c, + 0x92, 0x1a, 0x81, 0xba, 0x24, 0x29, 0xf0, 0xb5, 0xca, 0x8d, 0x58, 0x3f, 0x71, 0x94, 0xf4, 0x32, + 0xd2, 0x6d, 0xbc, 0x16, 0xe9, 0x61, 0x66, 0x46, 0x20, 0x0d, 0x3a, 0x9c, 0xd7, 0xa1, 0xd0, 0xb6, + 0x45, 0x8e, 0x24, 0x2f, 0x59, 0x07, 0x08, 0xec, 0x63, 0x73, 0x4a, 0x6e, 0xf1, 0x1f, 0xd8, 0x16, + 0x1b, 0xa9, 0xe4, 0x10, 0x60, 0xab, 0x1f, 0xa7, 0x80, 0x08, 0xed, 0x8f, 0x58, 0xc2, 0xe6, 0x01, + 0x61, 0x64, 0xa5, 0x8a, 0xca, 0xe7, 0xcd, 0x7e, 0x97, 0x79, 0xf0, 0x6e, 0x44, 0x1c, 0xea, 0xa1, + 0x15, 0x1a, 0xdf, 0xc6, 0xec, 0x78, 0xdb, 0xe8, 0x37, 0x8d, 0x73, 0xa3, 0x35, 0x8d, 0xd5, 0xdf, + 0x64, 0x20, 0x83, 0x9d, 0xcb, 0x64, 0x1c, 0x89, 0xbe, 0x01, 0x4b, 0x25, 0xde, 0x80, 0x9d, 0x4f, + 0x58, 0xaa, 0x1f, 0x4e, 0x22, 0xb6, 0x78, 0xc0, 0x2b, 0x9f, 0xfd, 0x3b, 0xe6, 0x81, 0x3d, 0xc9, + 0x8e, 0x79, 0x60, 0x31, 0xb5, 0x88, 0xc5, 0xc8, 0xe6, 0x94, 0x3f, 0x8e, 0xf9, 0xf5, 0x42, 0xc2, + 0xaf, 0x9f, 0x85, 0x52, 0xe4, 0xc9, 0x00, 0x06, 0x94, 0xa2, 0x06, 0xe1, 0x8b, 0x01, 0x1e, 0x6f, + 0x84, 0xa6, 0xf8, 0xb4, 0x7c, 0x05, 0x26, 0x00, 0x4d, 0x83, 0x5c, 0x80, 0xf2, 0x26, 0xed, 0x30, + 0x1d, 0xa3, 0x51, 0xf8, 0x14, 0x6c, 0x2a, 0x04, 0x8a, 0xdc, 0xdd, 0xf5, 0x18, 0xc5, 0xff, 0x5f, + 0x9a, 0x92, 0x97, 0x26, 0x3e, 0x6e, 0x62, 0x67, 0xc0, 0xb6, 0xda, 0xa6, 0x25, 0x02, 0x4a, 0x41, + 0x93, 0xa3, 0x44, 0xc3, 0x7e, 0x3a, 0xd9, 0xb0, 0x4f, 0x04, 0xa3, 0x99, 0xa3, 0xe4, 0x72, 0x95, + 0xb1, 0xba, 0x6d, 0xd7, 0xa0, 0x1a, 0xaa, 0x4b, 0xbc, 0xc9, 0x6d, 0x71, 0x61, 0xb9, 0x6c, 0xb3, + 0x28, 0xdb, 0x89, 0x8d, 0xfe, 0x27, 0xbb, 0x4d, 0x43, 0xfd, 0x76, 0x0a, 0xca, 0xc1, 0x9d, 0xde, + 0x6f, 0xbe, 0x63, 0xda, 0x16, 0x6b, 0xeb, 0x5f, 0x48, 0xf6, 0xc5, 0x03, 0xfc, 0x70, 0xa4, 0x41, + 0x2f, 0x28, 0x0e, 0x0c, 0x6d, 0xbe, 0x7f, 0xa4, 0x40, 0x31, 0xa0, 0x20, 0x17, 0x21, 0x8b, 0x9f, + 0x91, 0x7e, 0x77, 0xc0, 0xe3, 0x01, 0x31, 0xff, 0xd9, 0xf4, 0xdf, 0x2f, 0x43, 0x56, 0xbc, 0x24, + 0x78, 0x11, 0xb2, 0xd1, 0x97, 0x08, 0xfd, 0x8f, 0x04, 0xc4, 0xb4, 0xfa, 0xe5, 0x14, 0xcc, 0x63, + 0x56, 0x7f, 0xc4, 0xf7, 0x65, 0xe4, 0x3f, 0x21, 0x27, 0x62, 0xa5, 0x94, 0xf7, 0x56, 0xf4, 0x8b, + 0xfb, 0x7e, 0xa1, 0x3f, 0x90, 0x22, 0xba, 0x26, 0xd7, 0xab, 0x6d, 0xc0, 0xc9, 0xc1, 0x18, 0x61, + 0x57, 0x5a, 0x19, 0xd6, 0x95, 0x4e, 0x25, 0xba, 0xd2, 0xd1, 0xf3, 0x9b, 0x8e, 0x9f, 0x5f, 0xf5, + 0x8b, 0x29, 0x20, 0xb8, 0xee, 0x51, 0x2f, 0x6f, 0xc1, 0x1d, 0x2d, 0x3d, 0xe4, 0x8e, 0x96, 0x89, + 0xdf, 0x3a, 0x56, 0xfa, 0xef, 0x68, 0x23, 0x54, 0xd4, 0x93, 0x17, 0xb8, 0xdb, 0x03, 0x2e, 0x70, + 0x23, 0x14, 0xd1, 0x92, 0xb7, 0x3b, 0xf5, 0x09, 0xd4, 0xfa, 0xb5, 0xe0, 0x86, 0x99, 0x47, 0xe2, + 0x96, 0x71, 0xa6, 0x6f, 0x9f, 0x87, 0x5c, 0x5a, 0xbe, 0x90, 0x82, 0xd3, 0x38, 0x9f, 0xcc, 0xd4, + 0xc6, 0xaa, 0xe9, 0x3e, 0x49, 0x98, 0xd9, 0xcd, 0xbe, 0xcf, 0x0f, 0x59, 0xbe, 0x9e, 0x84, 0xc7, + 0x8d, 0x8c, 0xc1, 0x89, 0x81, 0x08, 0x93, 0xb5, 0xb1, 0xa5, 0xb7, 0x61, 0x4e, 0xb7, 0x3b, 0xf5, + 0xa7, 0xcc, 0xb1, 0x4d, 0xbd, 0x4d, 0xd7, 0xdd, 0x08, 0xfb, 0x4b, 0xc5, 0x55, 0xfc, 0xbd, 0xd8, + 0x35, 0x1f, 0x2a, 0xff, 0x95, 0xa6, 0x5d, 0xf3, 0x07, 0xa9, 0xcc, 0xea, 0xdd, 0x87, 0x4b, 0x3f, + 0x4a, 0xe5, 0xc4, 0xcc, 0x7a, 0x0e, 0x77, 0xf0, 0xca, 0x3f, 0x02, 0x00, 0x00, 0xff, 0xff, 0x8a, + 0x10, 0xce, 0x5c, 0xba, 0x3b, 0x00, 0x00, } diff --git a/vendor/github.com/heroiclabs/nakama-common/api/api.proto b/vendor/github.com/heroiclabs/nakama-common/api/api.proto index 60aba63d4424e84f51c5d2637bd38a019e64824a..fe34dc0150ed167b262547f2628d338744055b1a 100644 --- a/vendor/github.com/heroiclabs/nakama-common/api/api.proto +++ b/vendor/github.com/heroiclabs/nakama-common/api/api.proto @@ -46,6 +46,8 @@ message Account { string custom_id = 5; // The UNIX time when the user's email was verified. google.protobuf.Timestamp verify_time = 6; + // The UNIX time when the user's account was disabled/banned. + google.protobuf.Timestamp disable_time = 7; } // Send a custom ID to the server. Used with authenticate/link/unlink. diff --git a/vendor/github.com/heroiclabs/nakama-common/rtapi/realtime.pb.go b/vendor/github.com/heroiclabs/nakama-common/rtapi/realtime.pb.go index 0c90e6751b73bff5da169bcdb25c77d8059da786..2ea7c462ddd9ac81eec68634b750d23c16e39003 100644 --- a/vendor/github.com/heroiclabs/nakama-common/rtapi/realtime.pb.go +++ b/vendor/github.com/heroiclabs/nakama-common/rtapi/realtime.pb.go @@ -2732,9 +2732,7 @@ func init() { proto.RegisterType((*UserPresence)(nil), "nakama.realtime.UserPresence") } -func init() { - proto.RegisterFile("rtapi/realtime.proto", fileDescriptor_0163624496220f8c) -} +func init() { proto.RegisterFile("rtapi/realtime.proto", fileDescriptor_0163624496220f8c) } var fileDescriptor_0163624496220f8c = []byte{ // 2320 bytes of a gzipped FileDescriptorProto diff --git a/vendor/github.com/heroiclabs/nakama-common/runtime/runtime.go b/vendor/github.com/heroiclabs/nakama-common/runtime/runtime.go index fa0ca153d8a1d90664ef50ea51a3353e39014f87..90dd017229b131bc25d3a916ca861b32df508a76 100644 --- a/vendor/github.com/heroiclabs/nakama-common/runtime/runtime.go +++ b/vendor/github.com/heroiclabs/nakama-common/runtime/runtime.go @@ -88,7 +88,6 @@ package runtime import ( "context" "database/sql" - "github.com/heroiclabs/nakama-common/api" "github.com/heroiclabs/nakama-common/rtapi" ) @@ -104,6 +103,9 @@ const ( // "event", "run_once", "rpc", "before", "after", "match", "matchmaker", "leaderboard_reset", "tournament_reset", "tournament_end". RUNTIME_CTX_MODE = "execution_mode" + // The node ID where the current runtime context is executing. + RUNTIME_CTX_NODE = "node" + // Query params that was passed through from HTTP request. RUNTIME_CTX_QUERY_PARAMS = "query_params" @@ -791,7 +793,7 @@ type NakamaModule interface { AuthenticateDevice(ctx context.Context, id, username string, create bool) (string, string, bool, error) AuthenticateEmail(ctx context.Context, email, password, username string, create bool) (string, string, bool, error) AuthenticateFacebook(ctx context.Context, token string, importFriends bool, username string, create bool) (string, string, bool, error) - AuthenticateFacebookInstantGame(ctx context.Context, appSecret string, signedPlayerInfo string, username string, create bool) (string, string, bool, error) + AuthenticateFacebookInstantGame(ctx context.Context, signedPlayerInfo string, username string, create bool) (string, string, bool, error) AuthenticateGameCenter(ctx context.Context, playerID, bundleID string, timestamp int64, salt, signature, publicKeyUrl, username string, create bool) (string, string, bool, error) AuthenticateGoogle(ctx context.Context, token, username string, create bool) (string, string, bool, error) AuthenticateSteam(ctx context.Context, token, username string, create bool) (string, string, bool, error) @@ -810,6 +812,24 @@ type NakamaModule interface { UsersBanId(ctx context.Context, userIDs []string) error UsersUnbanId(ctx context.Context, userIDs []string) error + LinkCustom(ctx context.Context, userID, customID string) error + LinkDevice(ctx context.Context, userID, deviceID string) error + LinkEmail(ctx context.Context, userID, email, password string) error + LinkFacebook(ctx context.Context, userID, username, token string, importFriends bool) error + LinkFacebookInstantGame(ctx context.Context, userID, signedPlayerInfo string) error + LinkGameCenter(ctx context.Context, userID, playerID, bundleID string, timestamp int64, salt, signature, publicKeyUrl string) error + LinkGoogle(ctx context.Context, userID, token string) error + LinkSteam(ctx context.Context, userID, token string) error + + UnlinkCustom(ctx context.Context, userID, customID string) error + UnlinkDevice(ctx context.Context, userID, deviceID string) error + UnlinkEmail(ctx context.Context, userID, email string) error + UnlinkFacebook(ctx context.Context, userID, token string) error + UnlinkFacebookInstantGame(ctx context.Context, userID, signedPlayerInfo string) error + UnlinkGameCenter(ctx context.Context, userID, playerID, bundleID string, timestamp int64, salt, signature, publicKeyUrl string) error + UnlinkGoogle(ctx context.Context, userID, token string) error + UnlinkSteam(ctx context.Context, userID, token string) error + StreamUserList(mode uint8, subject, subcontext, label string, includeHidden, includeNotHidden bool) ([]Presence, error) StreamUserGet(mode uint8, subject, subcontext, label, userID, sessionID string) (PresenceMeta, error) StreamUserJoin(mode uint8, subject, subcontext, label, userID, sessionID string, hidden, persistence bool, status string) (bool, error) @@ -824,6 +844,7 @@ type NakamaModule interface { SessionDisconnect(ctx context.Context, sessionID, node string) error MatchCreate(ctx context.Context, module string, params map[string]interface{}) (string, error) + MatchGet(ctx context.Context, id string) (*api.Match, error) MatchList(ctx context.Context, limit int, authoritative bool, label string, minSize, maxSize *int, query string) ([]*api.Match, error) NotificationSend(ctx context.Context, userID, subject string, content map[string]interface{}, code int, sender string, persistent bool) error diff --git a/vendor/modules.txt b/vendor/modules.txt index 3d7d19716b8e3281cf74a3bba2db7751213626fe..23ad7f20fee3a4c694c6ec6543955b311d9388d6 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -95,7 +95,7 @@ github.com/grpc-ecosystem/grpc-gateway/internal github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger/options github.com/grpc-ecosystem/grpc-gateway/runtime github.com/grpc-ecosystem/grpc-gateway/utilities -# github.com/heroiclabs/nakama-common v1.4.0 +# github.com/heroiclabs/nakama-common v1.5.0 github.com/heroiclabs/nakama-common/api github.com/heroiclabs/nakama-common/rtapi github.com/heroiclabs/nakama-common/runtime