├── .gitignore ├── wasmtest ├── wasmtest.go └── wasmtest_test.go ├── nkapitest ├── proto │ ├── proto.proto │ ├── proto.go │ └── proto.pb.go └── main.go ├── ws_js.go ├── ws.go ├── LICENSE ├── code_string.go ├── example_test.go ├── .github └── workflows │ └── test.yml ├── README.md ├── proto_test.pb.go ├── go.mod ├── nakama_test.go ├── realtime.proto ├── realtime.go ├── conn.go └── nakama.proto /.gitignore: -------------------------------------------------------------------------------- 1 | /.cache/ 2 | *.txt 3 | -------------------------------------------------------------------------------- /wasmtest/wasmtest.go: -------------------------------------------------------------------------------- 1 | package wasmtest 2 | -------------------------------------------------------------------------------- /nkapitest/proto/proto.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package testdata.proto; 4 | 5 | option go_package = "github.com/ascii8/nakama-go/testdata/proto"; 6 | 7 | message Test { 8 | // AString is a test field. 9 | string AString = 1; 10 | // AInt is a test int64. 11 | int64 AInt = 2; 12 | } 13 | -------------------------------------------------------------------------------- /ws_js.go: -------------------------------------------------------------------------------- 1 | //go:build js 2 | 3 | package nakama 4 | 5 | import ( 6 | "net/http" 7 | 8 | "nhooyr.io/websocket" 9 | ) 10 | 11 | // buildWsOptions builds the websocket dial options. 12 | func buildWsOptions(httpClient *http.Client) *websocket.DialOptions { 13 | return &websocket.DialOptions{} 14 | } 15 | -------------------------------------------------------------------------------- /nkapitest/proto/proto.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | //go:generate protoc -I. --go_out=. --go_opt=paths=source_relative proto.proto 4 | //go:generate protoc -I. --go_out=../../ --go_opt=paths=source_relative --go_opt=Mproto.proto=github.com/ascii8/nakama-go;nakama proto.proto 5 | //go:generate mv ../../proto.pb.go ../../proto_test.pb.go 6 | -------------------------------------------------------------------------------- /ws.go: -------------------------------------------------------------------------------- 1 | //go:build !js 2 | 3 | package nakama 4 | 5 | import ( 6 | "net/http" 7 | 8 | "nhooyr.io/websocket" 9 | ) 10 | 11 | // buildWsOptions builds the websocket dial options. 12 | func buildWsOptions(httpClient *http.Client) *websocket.DialOptions { 13 | return &websocket.DialOptions{ 14 | HTTPClient: httpClient, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2020-2023 Tomas Z. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /code_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type Code -trimprefix Code"; DO NOT EDIT. 2 | 3 | package nakama 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[CodeOK-0] 12 | _ = x[CodeCanceled-1] 13 | _ = x[CodeUnknown-2] 14 | _ = x[CodeInvalidArgument-3] 15 | _ = x[CodeDeadlineExceeded-4] 16 | _ = x[CodeNotFound-5] 17 | _ = x[CodeAlreadyExists-6] 18 | _ = x[CodePermissionDenied-7] 19 | _ = x[CodeResourceExhausted-8] 20 | _ = x[CodeFailedPrecondition-9] 21 | _ = x[CodeAborted-10] 22 | _ = x[CodeOutOfRange-11] 23 | _ = x[CodeUnimplemented-12] 24 | _ = x[CodeInternal-13] 25 | _ = x[CodeUnavailable-14] 26 | _ = x[CodeDataLoss-15] 27 | _ = x[CodeUnauthenticated-16] 28 | } 29 | 30 | const _Code_name = "OKCanceledUnknownInvalidArgumentDeadlineExceededNotFoundAlreadyExistsPermissionDeniedResourceExhaustedFailedPreconditionAbortedOutOfRangeUnimplementedInternalUnavailableDataLossUnauthenticated" 31 | 32 | var _Code_index = [...]uint8{0, 2, 10, 17, 32, 48, 56, 69, 85, 102, 120, 127, 137, 150, 158, 169, 177, 192} 33 | 34 | func (i Code) String() string { 35 | if i >= Code(len(_Code_index)-1) { 36 | return "Code(" + strconv.FormatInt(int64(i), 10) + ")" 37 | } 38 | return _Code_name[_Code_index[i]:_Code_index[i+1]] 39 | } 40 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package nakama_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/ascii8/nakama-go" 9 | ) 10 | 11 | func Example() { 12 | const id = "6d0c9e83-8385-48a8-8601-060b8f6a3bf6" 13 | ctx, cancel := context.WithCancel(context.Background()) 14 | defer cancel() 15 | // create client 16 | cl := nakama.New(nakama.WithServerKey("nakama-go_server")) 17 | // authenticate 18 | if err := cl.AuthenticateDevice(ctx, id, true, ""); err != nil { 19 | log.Fatal(err) 20 | } 21 | // retrieve account 22 | account, err := cl.Account(ctx) 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | // list devices on the account 27 | for _, d := range account.Devices { 28 | fmt.Println("id:", d.Id) 29 | } 30 | // Output: 31 | // id: 6d0c9e83-8385-48a8-8601-060b8f6a3bf6 32 | } 33 | 34 | func ExampleRpc() { 35 | const amount = 1000 36 | type rewards struct { 37 | Rewards int64 `json:"rewards,omitempty"` 38 | } 39 | ctx, cancel := context.WithCancel(context.Background()) 40 | defer cancel() 41 | // create client 42 | cl := nakama.New(nakama.WithServerKey("nakama-go_server")) 43 | // create request and response 44 | res := new(rewards) 45 | req := nakama.Rpc("dailyRewards", rewards{Rewards: amount}, res) 46 | // execute rpc with http key 47 | if err := req.WithHttpKey("nakama-go").Do(ctx, cl); err != nil { 48 | log.Fatal(err) 49 | } 50 | fmt.Println("rewards:", res.Rewards) 51 | // Output: 52 | // rewards: 2000 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | env: 3 | PODMAN_HOST: unix:///tmp/podman.sock 4 | on: push 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: checkout 10 | uses: actions/checkout@v4 11 | - name: install go 12 | uses: actions/setup-go@v4 13 | with: 14 | go-version: stable 15 | - name: setup dependencies 16 | run: | 17 | sudo apt-get update 18 | sudo apt-get install -y libgpgme-dev libbtrfs-dev libdevmapper-dev 19 | - name: setup podman 20 | run: | 21 | export PODMAN_VERSION=$(go list -m all|grep github.com/containers/podman|awk '{print $2}') 22 | curl -fsSL -o podman-linux-amd64.tar.gz https://github.com/mgoltzsche/podman-static/releases/download/${PODMAN_VERSION}/podman-linux-amd64.tar.gz 23 | curl -fsSL -o podman-linux-amd64.tar.gz.asc https://github.com/mgoltzsche/podman-static/releases/download/${PODMAN_VERSION}/podman-linux-amd64.tar.gz.asc 24 | gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys 0CCF102C4F95D89E583FF1D4F8B5AF50344BB503 25 | gpg --batch --verify podman-linux-amd64.tar.gz.asc podman-linux-amd64.tar.gz 26 | tar -xzf podman-linux-amd64.tar.gz 27 | sudo cp -r podman-linux-amd64/usr podman-linux-amd64/etc / 28 | echo "PODMAN_HOST: $PODMAN_HOST" 29 | /usr/local/bin/podman system service --time=0 $PODMAN_HOST & 30 | export CONTAINER_HOST=$PODMAN_HOST 31 | /usr/local/bin/podman --remote version 32 | - name: test 33 | run: | 34 | TRACE=1 go test -v -timeout=1h 35 | -------------------------------------------------------------------------------- /nkapitest/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "encoding/json" 7 | "fmt" 8 | "strings" 9 | 10 | testpb "github.com/ascii8/nakama-go/nkapitest/proto" 11 | "github.com/heroiclabs/nakama-common/runtime" 12 | "google.golang.org/protobuf/encoding/protojson" 13 | ) 14 | 15 | func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error { 16 | if err := initializer.RegisterRpc("dailyRewards", dailyRewards); err != nil { 17 | return err 18 | } 19 | if err := initializer.RegisterRpc("protoTest", protoTest); err != nil { 20 | return err 21 | } 22 | return nil 23 | } 24 | 25 | func dailyRewards(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payloadstr string) (string, error) { 26 | // decode request 27 | dec := json.NewDecoder(strings.NewReader(payloadstr)) 28 | dec.DisallowUnknownFields() 29 | var req Rewards 30 | if err := dec.Decode(&req); err != nil { 31 | return "", err 32 | } 33 | logger.WithField("req", req).Debug("dailyRewards") 34 | res := Rewards{ 35 | Rewards: req.Rewards * 2, 36 | } 37 | logger.WithField("res", res).Debug("dailyRewards") 38 | // encode response 39 | buf, err := json.Marshal(res) 40 | if err != nil { 41 | return "", err 42 | } 43 | return string(buf), nil 44 | } 45 | 46 | type Rewards struct { 47 | Rewards int64 `json:"rewards,omitempty"` 48 | } 49 | 50 | func protoTest(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payloadstr string) (string, error) { 51 | req := new(testpb.Test) 52 | if err := protojson.Unmarshal([]byte(payloadstr), req); err != nil { 53 | return "", fmt.Errorf("unable to unmarshal protobuf message: %w", err) 54 | } 55 | logger.WithField("req", req).Debug("protoTest") 56 | res := &testpb.Test{ 57 | AString: "hello " + req.AString, 58 | AInt: 2 * req.AInt, 59 | } 60 | logger.WithField("res", res).Debug("protoTest") 61 | buf, err := protojson.Marshal(res) 62 | if err != nil { 63 | return "", err 64 | } 65 | return string(buf), nil 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | A Go web and realtime client package for the Nakama game server. Works with 4 | WASM builds. 5 | 6 | [![Tests](https://github.com/ascii8/nakama-go/workflows/Test/badge.svg)](https://github.com/ascii8/nakama-go/actions?query=workflow%3ATest) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/ascii8/nakama-go)](https://goreportcard.com/report/github.com/ascii8/nakama-go) 8 | [![Reference](https://pkg.go.dev/badge/github.com/ascii8/nakama-go.svg)](https://pkg.go.dev/github.com/ascii8/nakama-go) 9 | [![Releases](https://img.shields.io/github/v/release/ascii8/nakama-go?display_name=tag&sort=semver)](https://github.com/ascii8/nakama-go/releases) 10 | 11 | ## Using 12 | 13 | ```sh 14 | go get github.com/ascii8/nakama-go 15 | ``` 16 | 17 | ## Quickstart 18 | 19 | ```go 20 | package nakama_test 21 | 22 | import ( 23 | "context" 24 | "fmt" 25 | "log" 26 | 27 | "github.com/ascii8/nakama-go" 28 | ) 29 | 30 | func Example() { 31 | const id = "6d0c9e83-8385-48a8-8601-060b8f6a3bf6" 32 | ctx, cancel := context.WithCancel(context.Background()) 33 | defer cancel() 34 | // create client 35 | cl := nakama.New(nakama.WithServerKey("apitest_server")) 36 | // authenticate 37 | if err := cl.AuthenticateDevice(ctx, id, true, ""); err != nil { 38 | log.Fatal(err) 39 | } 40 | // retrieve account 41 | account, err := cl.Account(ctx) 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | // list devices on the account 46 | for _, d := range account.Devices { 47 | fmt.Println("id:", d.Id) 48 | } 49 | // Output: 50 | // id: 6d0c9e83-8385-48a8-8601-060b8f6a3bf6 51 | } 52 | 53 | ``` 54 | 55 | ## Examples 56 | 57 | See [`github.com/ascii8/xoxo-go`](https://github.com/ascii8/xoxo-go) for a 58 | demonstration of end-to-end unit tests using `nktest`, and "real world" 59 | examples of pure Go clients (e.g., Ebitengine) built using this client 60 | package. 61 | 62 | See the [Go package documentation](https://pkg.go.dev/github.com/ascii8/nakama-go) 63 | for other examples. 64 | 65 | ## Notes 66 | 67 | Run browser tests: 68 | 69 | ```sh 70 | # setup wasmbrowsertest 71 | $ go install github.com/agnivade/wasmbrowsertest@latest 72 | $ cd $GOPATH/bin && ln -s wasmbrowsertest go_js_wasm_exec 73 | 74 | # run the wasmtests 75 | $ cd /path/to/nakama-go/wasmtest 76 | $ GOOS=js GOARCH=wasm go test -v 77 | ``` 78 | 79 | ## Related Links 80 | 81 | * [`github.com/ascii8/nktest`](https://github.com/ascii8/nktest) - a Nakama test runner 82 | * [`github.com/ascii8/xoxo-go`](https://github.com/ascii8/xoxo-go) - a pure Go version of Nakama's XOXO example, demonstrating end-to-end unit tests, and providing multiple example clients using this package. Has example Ebitengine client 83 | -------------------------------------------------------------------------------- /wasmtest/wasmtest_test.go: -------------------------------------------------------------------------------- 1 | package wasmtest 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | "testing" 8 | "time" 9 | 10 | "github.com/ascii8/nakama-go" 11 | "github.com/google/uuid" 12 | ) 13 | 14 | func TestPersist(t *testing.T) { 15 | ctx, cancel := context.WithCancel(context.Background()) 16 | defer cancel() 17 | cl := newClient(t) 18 | conn := createAccountAndConn(ctx, t, cl, false, nakama.WithConnPersist(true)) 19 | ch := make(chan error) 20 | conn.DisconnectHandler = func(_ context.Context, err error) { 21 | ch <- err 22 | } 23 | if err := conn.Open(ctx); err != nil { 24 | t.Fatalf("expected no error, got: %v", err) 25 | } 26 | <-time.After(1500 * time.Millisecond) 27 | if !conn.Connected() { 28 | t.Errorf("expected conn to be connected") 29 | } 30 | if err := conn.CloseWithStopErr(true, false, errors.New("STOPPING")); err != nil { 31 | t.Fatalf("expected no error, got: %v", err) 32 | } 33 | select { 34 | case <-ctx.Done(): 35 | t.Fatalf("expected no error, got: %v", ctx.Err()) 36 | case <-time.After(1 * time.Minute): 37 | t.Fatalf("expected disconnected error") 38 | case err := <-ch: 39 | switch { 40 | case err == nil: 41 | t.Fatalf("expected disconnected error") 42 | case err.Error() != "STOPPING": 43 | t.Errorf("expected STOPPING error") 44 | } 45 | } 46 | switch { 47 | case conn.Connected(): 48 | t.Errorf("expected conn.Connected() == false") 49 | } 50 | } 51 | 52 | func newClient(t *testing.T) *nakama.Client { 53 | const urlstr = "http://127.0.0.1:7350" 54 | const serverKey = "nakama-go_server" 55 | t.Logf("url: %s", urlstr) 56 | opts := append([]nakama.Option{ 57 | nakama.WithURL(urlstr), 58 | nakama.WithServerKey(serverKey), 59 | nakama.WithTransport(&http.Transport{ 60 | DisableCompression: true, 61 | }), 62 | }) 63 | return nakama.New(opts...) 64 | } 65 | 66 | func createAccount(ctx context.Context, t *testing.T, cl *nakama.Client) { 67 | deviceId := uuid.New().String() 68 | t.Logf("registering: %s", deviceId) 69 | if err := cl.AuthenticateDevice(ctx, deviceId, true, ""); err != nil { 70 | t.Fatalf("expected no error: got: %v", err) 71 | } 72 | expiry := cl.SessionExpiry() 73 | t.Logf("expiry: %s", cl.SessionExpiry()) 74 | if expiry.IsZero() || expiry.Before(time.Now()) { 75 | t.Fatalf("expected non-zero expiry in the future, got: %s", expiry) 76 | } 77 | res, err := cl.Account(ctx) 78 | if err != nil { 79 | t.Fatalf("expected no error, got: %v", err) 80 | } 81 | t.Logf("account: %+v", res) 82 | if len(res.Devices) == 0 { 83 | t.Fatalf("expected there to be at least one device") 84 | } 85 | found := false 86 | for _, d := range res.Devices { 87 | if d.Id == deviceId { 88 | found = true 89 | break 90 | } 91 | } 92 | if !found { 93 | t.Fatalf("expected accountRes.Devices to contain %s", deviceId) 94 | } 95 | } 96 | 97 | func createAccountAndConn(ctx context.Context, t *testing.T, cl *nakama.Client, check bool, opts ...nakama.ConnOption) *nakama.Conn { 98 | createAccount(ctx, t, cl) 99 | conn, err := cl.NewConn(ctx, append([]nakama.ConnOption{nakama.WithConnFormat("json")}, opts...)...) 100 | if err != nil { 101 | t.Fatalf("expected no error, got: %v", err) 102 | } 103 | if check && conn.Connected() != true { 104 | t.Fatalf("expected conn.Connected() == true") 105 | } 106 | return conn 107 | } 108 | -------------------------------------------------------------------------------- /proto_test.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.28.1 4 | // protoc v3.21.12 5 | // source: proto.proto 6 | 7 | package nakama 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type Test struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | 28 | // AString is a test field. 29 | AString string `protobuf:"bytes,1,opt,name=AString,proto3" json:"AString,omitempty"` 30 | // AInt is a test int64. 31 | AInt int64 `protobuf:"varint,2,opt,name=AInt,proto3" json:"AInt,omitempty"` 32 | } 33 | 34 | func (x *Test) Reset() { 35 | *x = Test{} 36 | if protoimpl.UnsafeEnabled { 37 | mi := &file_proto_proto_msgTypes[0] 38 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 39 | ms.StoreMessageInfo(mi) 40 | } 41 | } 42 | 43 | func (x *Test) String() string { 44 | return protoimpl.X.MessageStringOf(x) 45 | } 46 | 47 | func (*Test) ProtoMessage() {} 48 | 49 | func (x *Test) ProtoReflect() protoreflect.Message { 50 | mi := &file_proto_proto_msgTypes[0] 51 | if protoimpl.UnsafeEnabled && x != nil { 52 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 53 | if ms.LoadMessageInfo() == nil { 54 | ms.StoreMessageInfo(mi) 55 | } 56 | return ms 57 | } 58 | return mi.MessageOf(x) 59 | } 60 | 61 | // Deprecated: Use Test.ProtoReflect.Descriptor instead. 62 | func (*Test) Descriptor() ([]byte, []int) { 63 | return file_proto_proto_rawDescGZIP(), []int{0} 64 | } 65 | 66 | func (x *Test) GetAString() string { 67 | if x != nil { 68 | return x.AString 69 | } 70 | return "" 71 | } 72 | 73 | func (x *Test) GetAInt() int64 { 74 | if x != nil { 75 | return x.AInt 76 | } 77 | return 0 78 | } 79 | 80 | var File_proto_proto protoreflect.FileDescriptor 81 | 82 | var file_proto_proto_rawDesc = []byte{ 83 | 0x0a, 0x0b, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0e, 0x74, 84 | 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x34, 0x0a, 85 | 0x04, 0x54, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x41, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 86 | 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x41, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x12, 87 | 0x12, 0x0a, 0x04, 0x41, 0x49, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x41, 88 | 0x49, 0x6e, 0x74, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 89 | 0x6d, 0x2f, 0x61, 0x73, 0x63, 0x69, 0x69, 0x38, 0x2f, 0x6e, 0x61, 0x6b, 0x61, 0x6d, 0x61, 0x2d, 90 | 0x67, 0x6f, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x2f, 0x70, 0x72, 0x6f, 0x74, 91 | 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 92 | } 93 | 94 | var ( 95 | file_proto_proto_rawDescOnce sync.Once 96 | file_proto_proto_rawDescData = file_proto_proto_rawDesc 97 | ) 98 | 99 | func file_proto_proto_rawDescGZIP() []byte { 100 | file_proto_proto_rawDescOnce.Do(func() { 101 | file_proto_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_proto_rawDescData) 102 | }) 103 | return file_proto_proto_rawDescData 104 | } 105 | 106 | var file_proto_proto_msgTypes = make([]protoimpl.MessageInfo, 1) 107 | var file_proto_proto_goTypes = []interface{}{ 108 | (*Test)(nil), // 0: testdata.proto.Test 109 | } 110 | var file_proto_proto_depIdxs = []int32{ 111 | 0, // [0:0] is the sub-list for method output_type 112 | 0, // [0:0] is the sub-list for method input_type 113 | 0, // [0:0] is the sub-list for extension type_name 114 | 0, // [0:0] is the sub-list for extension extendee 115 | 0, // [0:0] is the sub-list for field type_name 116 | } 117 | 118 | func init() { file_proto_proto_init() } 119 | func file_proto_proto_init() { 120 | if File_proto_proto != nil { 121 | return 122 | } 123 | if !protoimpl.UnsafeEnabled { 124 | file_proto_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 125 | switch v := v.(*Test); i { 126 | case 0: 127 | return &v.state 128 | case 1: 129 | return &v.sizeCache 130 | case 2: 131 | return &v.unknownFields 132 | default: 133 | return nil 134 | } 135 | } 136 | } 137 | type x struct{} 138 | out := protoimpl.TypeBuilder{ 139 | File: protoimpl.DescBuilder{ 140 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 141 | RawDescriptor: file_proto_proto_rawDesc, 142 | NumEnums: 0, 143 | NumMessages: 1, 144 | NumExtensions: 0, 145 | NumServices: 0, 146 | }, 147 | GoTypes: file_proto_proto_goTypes, 148 | DependencyIndexes: file_proto_proto_depIdxs, 149 | MessageInfos: file_proto_proto_msgTypes, 150 | }.Build() 151 | File_proto_proto = out.File 152 | file_proto_proto_rawDesc = nil 153 | file_proto_proto_goTypes = nil 154 | file_proto_proto_depIdxs = nil 155 | } 156 | -------------------------------------------------------------------------------- /nkapitest/proto/proto.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.28.1 4 | // protoc v3.21.12 5 | // source: proto.proto 6 | 7 | package proto 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type Test struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | 28 | // AString is a test field. 29 | AString string `protobuf:"bytes,1,opt,name=AString,proto3" json:"AString,omitempty"` 30 | // AInt is a test int64. 31 | AInt int64 `protobuf:"varint,2,opt,name=AInt,proto3" json:"AInt,omitempty"` 32 | } 33 | 34 | func (x *Test) Reset() { 35 | *x = Test{} 36 | if protoimpl.UnsafeEnabled { 37 | mi := &file_proto_proto_msgTypes[0] 38 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 39 | ms.StoreMessageInfo(mi) 40 | } 41 | } 42 | 43 | func (x *Test) String() string { 44 | return protoimpl.X.MessageStringOf(x) 45 | } 46 | 47 | func (*Test) ProtoMessage() {} 48 | 49 | func (x *Test) ProtoReflect() protoreflect.Message { 50 | mi := &file_proto_proto_msgTypes[0] 51 | if protoimpl.UnsafeEnabled && x != nil { 52 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 53 | if ms.LoadMessageInfo() == nil { 54 | ms.StoreMessageInfo(mi) 55 | } 56 | return ms 57 | } 58 | return mi.MessageOf(x) 59 | } 60 | 61 | // Deprecated: Use Test.ProtoReflect.Descriptor instead. 62 | func (*Test) Descriptor() ([]byte, []int) { 63 | return file_proto_proto_rawDescGZIP(), []int{0} 64 | } 65 | 66 | func (x *Test) GetAString() string { 67 | if x != nil { 68 | return x.AString 69 | } 70 | return "" 71 | } 72 | 73 | func (x *Test) GetAInt() int64 { 74 | if x != nil { 75 | return x.AInt 76 | } 77 | return 0 78 | } 79 | 80 | var File_proto_proto protoreflect.FileDescriptor 81 | 82 | var file_proto_proto_rawDesc = []byte{ 83 | 0x0a, 0x0b, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0e, 0x74, 84 | 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x34, 0x0a, 85 | 0x04, 0x54, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x41, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 86 | 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x41, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x12, 87 | 0x12, 0x0a, 0x04, 0x41, 0x49, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x41, 88 | 0x49, 0x6e, 0x74, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 89 | 0x6d, 0x2f, 0x61, 0x73, 0x63, 0x69, 0x69, 0x38, 0x2f, 0x6e, 0x61, 0x6b, 0x61, 0x6d, 0x61, 0x2d, 90 | 0x67, 0x6f, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x2f, 0x70, 0x72, 0x6f, 0x74, 91 | 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 92 | } 93 | 94 | var ( 95 | file_proto_proto_rawDescOnce sync.Once 96 | file_proto_proto_rawDescData = file_proto_proto_rawDesc 97 | ) 98 | 99 | func file_proto_proto_rawDescGZIP() []byte { 100 | file_proto_proto_rawDescOnce.Do(func() { 101 | file_proto_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_proto_rawDescData) 102 | }) 103 | return file_proto_proto_rawDescData 104 | } 105 | 106 | var file_proto_proto_msgTypes = make([]protoimpl.MessageInfo, 1) 107 | var file_proto_proto_goTypes = []interface{}{ 108 | (*Test)(nil), // 0: testdata.proto.Test 109 | } 110 | var file_proto_proto_depIdxs = []int32{ 111 | 0, // [0:0] is the sub-list for method output_type 112 | 0, // [0:0] is the sub-list for method input_type 113 | 0, // [0:0] is the sub-list for extension type_name 114 | 0, // [0:0] is the sub-list for extension extendee 115 | 0, // [0:0] is the sub-list for field type_name 116 | } 117 | 118 | func init() { file_proto_proto_init() } 119 | func file_proto_proto_init() { 120 | if File_proto_proto != nil { 121 | return 122 | } 123 | if !protoimpl.UnsafeEnabled { 124 | file_proto_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 125 | switch v := v.(*Test); i { 126 | case 0: 127 | return &v.state 128 | case 1: 129 | return &v.sizeCache 130 | case 2: 131 | return &v.unknownFields 132 | default: 133 | return nil 134 | } 135 | } 136 | } 137 | type x struct{} 138 | out := protoimpl.TypeBuilder{ 139 | File: protoimpl.DescBuilder{ 140 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 141 | RawDescriptor: file_proto_proto_rawDesc, 142 | NumEnums: 0, 143 | NumMessages: 1, 144 | NumExtensions: 0, 145 | NumServices: 0, 146 | }, 147 | GoTypes: file_proto_proto_goTypes, 148 | DependencyIndexes: file_proto_proto_depIdxs, 149 | MessageInfos: file_proto_proto_msgTypes, 150 | }.Build() 151 | File_proto_proto = out.File 152 | file_proto_proto_rawDesc = nil 153 | file_proto_proto_goTypes = nil 154 | file_proto_proto_depIdxs = nil 155 | } 156 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ascii8/nakama-go 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/ascii8/nktest v0.12.3 7 | github.com/google/uuid v1.6.0 8 | github.com/heroiclabs/nakama-common v1.31.0 9 | golang.org/x/net v0.19.0 10 | google.golang.org/protobuf v1.31.0 11 | nhooyr.io/websocket v1.8.10 12 | ) 13 | 14 | require ( 15 | dario.cat/mergo v1.0.0 // indirect 16 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect 17 | github.com/BurntSushi/toml v1.3.2 // indirect 18 | github.com/Microsoft/go-winio v0.6.1 // indirect 19 | github.com/Microsoft/hcsshim v0.12.0-rc.1 // indirect 20 | github.com/VividCortex/ewma v1.2.0 // indirect 21 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 22 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect 23 | github.com/blang/semver/v4 v4.0.0 // indirect 24 | github.com/chzyer/readline v1.5.1 // indirect 25 | github.com/cilium/ebpf v0.9.3 // indirect 26 | github.com/containerd/cgroups/v3 v3.0.2 // indirect 27 | github.com/containerd/containerd v1.7.9 // indirect 28 | github.com/containerd/log v0.1.0 // indirect 29 | github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect 30 | github.com/containers/buildah v1.33.3 // indirect 31 | github.com/containers/common v0.57.2 // indirect 32 | github.com/containers/image/v5 v5.29.1 // indirect 33 | github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect 34 | github.com/containers/ocicrypt v1.1.9 // indirect 35 | github.com/containers/podman/v4 v4.8.3 // indirect 36 | github.com/containers/psgo v1.8.0 // indirect 37 | github.com/containers/storage v1.51.0 // indirect 38 | github.com/coreos/go-systemd/v22 v22.5.1-0.20231103132048-7d375ecc2b09 // indirect 39 | github.com/cyberphone/json-canonicalization v0.0.0-20231011164504-785e29786b46 // indirect 40 | github.com/cyphar/filepath-securejoin v0.2.4 // indirect 41 | github.com/disiqueira/gotree/v3 v3.0.2 // indirect 42 | github.com/distribution/reference v0.5.0 // indirect 43 | github.com/docker/distribution v2.8.3+incompatible // indirect 44 | github.com/docker/docker v24.0.7+incompatible // indirect 45 | github.com/docker/docker-credential-helpers v0.8.0 // indirect 46 | github.com/docker/go-connections v0.4.1-0.20231031175723-0b8c1f4e07a0 // indirect 47 | github.com/docker/go-units v0.5.0 // indirect 48 | github.com/fsnotify/fsnotify v1.7.0 // indirect 49 | github.com/go-jose/go-jose/v3 v3.0.1 // indirect 50 | github.com/go-openapi/analysis v0.21.4 // indirect 51 | github.com/go-openapi/errors v0.20.4 // indirect 52 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 53 | github.com/go-openapi/jsonreference v0.20.2 // indirect 54 | github.com/go-openapi/loads v0.21.2 // indirect 55 | github.com/go-openapi/runtime v0.26.0 // indirect 56 | github.com/go-openapi/spec v0.20.9 // indirect 57 | github.com/go-openapi/strfmt v0.21.7 // indirect 58 | github.com/go-openapi/swag v0.22.4 // indirect 59 | github.com/go-openapi/validate v0.22.1 // indirect 60 | github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect 61 | github.com/gogo/protobuf v1.3.2 // indirect 62 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 63 | github.com/golang/protobuf v1.5.3 // indirect 64 | github.com/google/go-containerregistry v0.16.1 // indirect 65 | github.com/google/go-intervals v0.0.2 // indirect 66 | github.com/gorilla/mux v1.8.1 // indirect 67 | github.com/gorilla/schema v1.2.0 // indirect 68 | github.com/gorilla/websocket v1.5.1 // indirect 69 | github.com/hashicorp/errwrap v1.1.0 // indirect 70 | github.com/hashicorp/go-multierror v1.1.1 // indirect 71 | github.com/jinzhu/copier v0.4.0 // indirect 72 | github.com/josharian/intern v1.0.0 // indirect 73 | github.com/json-iterator/go v1.1.12 // indirect 74 | github.com/klauspost/compress v1.17.5 // indirect 75 | github.com/klauspost/pgzip v1.2.6 // indirect 76 | github.com/kr/fs v0.1.0 // indirect 77 | github.com/letsencrypt/boulder v0.0.0-20230213213521-fdfea0d469b6 // indirect 78 | github.com/lib/pq v1.10.9 // indirect 79 | github.com/mailru/easyjson v0.7.7 // indirect 80 | github.com/manifoldco/promptui v0.9.0 // indirect 81 | github.com/mattn/go-colorable v0.1.13 // indirect 82 | github.com/mattn/go-isatty v0.0.20 // indirect 83 | github.com/mattn/go-runewidth v0.0.15 // indirect 84 | github.com/mattn/go-shellwords v1.0.12 // indirect 85 | github.com/mattn/go-sqlite3 v1.14.18 // indirect 86 | github.com/miekg/pkcs11 v1.1.1 // indirect 87 | github.com/mistifyio/go-zfs/v3 v3.0.1 // indirect 88 | github.com/mitchellh/mapstructure v1.5.0 // indirect 89 | github.com/moby/sys/mountinfo v0.7.1 // indirect 90 | github.com/moby/term v0.5.0 // indirect 91 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 92 | github.com/modern-go/reflect2 v1.0.2 // indirect 93 | github.com/morikuni/aec v1.0.0 // indirect 94 | github.com/nxadm/tail v1.4.11 // indirect 95 | github.com/oklog/ulid v1.3.1 // indirect 96 | github.com/opencontainers/go-digest v1.0.0 // indirect 97 | github.com/opencontainers/image-spec v1.1.0-rc5 // indirect 98 | github.com/opencontainers/runc v1.1.10 // indirect 99 | github.com/opencontainers/runtime-spec v1.1.1-0.20230922153023-c0e90434df2a // indirect 100 | github.com/opencontainers/runtime-tools v0.9.1-0.20230914150019-408c51e934dc // indirect 101 | github.com/opencontainers/selinux v1.11.0 // indirect 102 | github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f // indirect 103 | github.com/pkg/errors v0.9.1 // indirect 104 | github.com/pkg/sftp v1.13.6 // indirect 105 | github.com/proglottis/gpgme v0.1.3 // indirect 106 | github.com/rivo/uniseg v0.4.4 // indirect 107 | github.com/rs/zerolog v1.31.0 // indirect 108 | github.com/secure-systems-lab/go-securesystemslib v0.7.0 // indirect 109 | github.com/sigstore/fulcio v1.4.3 // indirect 110 | github.com/sigstore/rekor v1.2.2 // indirect 111 | github.com/sigstore/sigstore v1.7.5 // indirect 112 | github.com/sirupsen/logrus v1.9.3 // indirect 113 | github.com/spf13/pflag v1.0.5 // indirect 114 | github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980 // indirect 115 | github.com/sylabs/sif/v2 v2.15.0 // indirect 116 | github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect 117 | github.com/tchap/go-patricia/v2 v2.3.1 // indirect 118 | github.com/teivah/onecontext v1.3.0 // indirect 119 | github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect 120 | github.com/ulikunitz/xz v0.5.11 // indirect 121 | github.com/vbatts/tar-split v0.11.5 // indirect 122 | github.com/vbauerster/mpb/v8 v8.6.2 // indirect 123 | github.com/yookoala/realpath v1.0.0 // indirect 124 | go.mongodb.org/mongo-driver v1.11.3 // indirect 125 | go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect 126 | go.opencensus.io v0.24.0 // indirect 127 | golang.org/x/crypto v0.17.0 // indirect 128 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect 129 | golang.org/x/mod v0.14.0 // indirect 130 | golang.org/x/sync v0.6.0 // indirect 131 | golang.org/x/sys v0.15.0 // indirect 132 | golang.org/x/term v0.15.0 // indirect 133 | golang.org/x/text v0.14.0 // indirect 134 | golang.org/x/tools v0.14.0 // indirect 135 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect 136 | google.golang.org/grpc v1.58.3 // indirect 137 | gopkg.in/go-jose/go-jose.v2 v2.6.1 // indirect 138 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 139 | gopkg.in/yaml.v3 v3.0.1 // indirect 140 | sigs.k8s.io/yaml v1.4.0 // indirect 141 | tags.cncf.io/container-device-interface v0.6.2 // indirect 142 | ) 143 | -------------------------------------------------------------------------------- /nakama_test.go: -------------------------------------------------------------------------------- 1 | package nakama 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "net/http" 8 | "os" 9 | "reflect" 10 | "testing" 11 | "time" 12 | 13 | "github.com/ascii8/nktest" 14 | "github.com/google/uuid" 15 | ) 16 | 17 | // TestMain handles setting up and tearing down the postgres and nakama 18 | // containers. 19 | func TestMain(m *testing.M) { 20 | ctx := context.Background() 21 | ctx = nktest.WithAlwaysPullFromEnv(ctx, "PULL") 22 | ctx = nktest.WithUnderCIFromEnv(ctx, "CI") 23 | ctx = nktest.WithHostPortMap(ctx) 24 | var opts []nktest.BuildConfigOption 25 | if os.Getenv("CI") == "" { 26 | opts = append(opts, nktest.WithDefaultGoEnv(), nktest.WithDefaultGoVolumes()) 27 | } 28 | nktest.Main(ctx, m, 29 | nktest.WithDir("."), 30 | nktest.WithBuildConfig("./nkapitest", opts...), 31 | ) 32 | } 33 | 34 | func TestHealthcheck(t *testing.T) { 35 | ctx, cancel, nk := nktest.WithCancel(context.Background(), t) 36 | defer cancel() 37 | cl := newClient(ctx, t, nk) 38 | if err := cl.Healthcheck(ctx); err != nil { 39 | t.Errorf("expected no error, got: %v", err) 40 | } 41 | } 42 | 43 | func TestRpc(t *testing.T) { 44 | ctx, cancel, nk := nktest.WithCancel(context.Background(), t) 45 | defer cancel() 46 | const amount int64 = 1000 47 | cl := newClient(ctx, t, nk) 48 | var res rewards 49 | req := Rpc( 50 | "dailyRewards", 51 | rewards{ 52 | Rewards: amount, 53 | }, 54 | &res, 55 | ). 56 | WithHttpKey(nk.Name()) 57 | if err := req.Do(ctx, cl); err != nil { 58 | t.Fatalf("expected no error, got: %v", err) 59 | } 60 | t.Logf("rewards: %d", res.Rewards) 61 | if res.Rewards != 2*amount { 62 | t.Errorf("expected %d, got: %d", 2*amount, res.Rewards) 63 | } 64 | } 65 | 66 | func TestRpcProtoEncodeDecode(t *testing.T) { 67 | ctx, cancel, nk := nktest.WithCancel(context.Background(), t) 68 | defer cancel() 69 | const name string = "bob" 70 | const amount int64 = 1000 71 | cl := newClient(ctx, t, nk) 72 | msg := &Test{ 73 | AString: name, 74 | AInt: amount, 75 | } 76 | res := new(Test) 77 | req := Rpc("protoTest", msg, res) 78 | if err := req.WithHttpKey(nk.Name()).Do(ctx, cl); err != nil { 79 | t.Fatalf("expected no error, got: %v", err) 80 | } 81 | t.Logf("AString: %s", res.AString) 82 | t.Logf("AInt: %d", res.AInt) 83 | if res.AString != "hello "+name { 84 | t.Errorf("expected %q, got: %q", "hello "+name, res.AString) 85 | } 86 | if res.AInt != 2*amount { 87 | t.Errorf("expected %d, got: %d", 2*amount, res.AInt) 88 | } 89 | } 90 | 91 | func TestAuthenticateDevice(t *testing.T) { 92 | ctx, cancel, nk := nktest.WithCancel(context.Background(), t) 93 | defer cancel() 94 | cl := newClient(ctx, t, nk, WithServerKey(nk.ServerKey())) 95 | createAccount(ctx, t, cl) 96 | } 97 | 98 | func TestPing(t *testing.T) { 99 | ctx, cancel, nk := nktest.WithCancel(context.Background(), t) 100 | defer cancel() 101 | cl := newClient(ctx, t, nk, WithServerKey(nk.ServerKey())) 102 | conn := createAccountAndConn(ctx, t, cl, true) 103 | defer conn.Close() 104 | if err := conn.Ping(ctx); err != nil { 105 | t.Errorf("expected no error, got: %v", err) 106 | } 107 | if len(conn.m) != 0 { 108 | t.Errorf("expected len(conn.l) == 0, got: %d", len(conn.m)) 109 | } 110 | errc := make(chan error, 1) 111 | conn.PingAsync(ctx, func(err error) { 112 | defer close(errc) 113 | errc <- err 114 | }) 115 | select { 116 | case <-ctx.Done(): 117 | case err := <-errc: 118 | if err != nil { 119 | t.Errorf("expected no error, got: %v", err) 120 | } 121 | } 122 | if len(conn.m) != 0 { 123 | t.Errorf("expected len(conn.l) == 0, got: %d", len(conn.m)) 124 | } 125 | } 126 | 127 | func TestMatch(t *testing.T) { 128 | ctx, cancel, nk := nktest.WithCancel(context.Background(), t) 129 | defer cancel() 130 | cl1 := newClient(ctx, t, nk) 131 | conn1 := createAccountAndConn(ctx, t, cl1, true) 132 | defer conn1.Close() 133 | a1, err := cl1.Account(ctx) 134 | if err != nil { 135 | t.Fatalf("expected no error, got: %v", err) 136 | } 137 | t.Logf("account1: %+v", a1) 138 | joinCh := make(chan *MatchPresenceEventMsg, 1) 139 | defer close(joinCh) 140 | conn1.MatchPresenceEventHandler = func(_ context.Context, msg *MatchPresenceEventMsg) { 141 | joinCh <- msg 142 | } 143 | m1, err := conn1.MatchCreate(ctx, "") 144 | switch { 145 | case err != nil: 146 | t.Fatalf("expected no error, got: %v", err) 147 | case m1.MatchId == "": 148 | t.Fatalf("expected non-empty m1.MatchId") 149 | case m1.Authoritative: 150 | t.Errorf("expected m1.Authoritative == false") 151 | case m1.Size == 0: 152 | t.Errorf("expected m1.Size != 0") 153 | case m1.Self.UserId != a1.User.Id: 154 | t.Errorf("expected m1.Self.UserId == a1.User.Id") 155 | } 156 | for _, p := range m1.Presences { 157 | t.Logf("p %s: %v", p.UserId, p.Status) 158 | } 159 | cl2 := newClient(ctx, t, nk) 160 | conn2 := createAccountAndConn(ctx, t, cl2, true) 161 | defer conn2.Close() 162 | dataCh := make(chan *MatchDataMsg, 1) 163 | defer close(dataCh) 164 | conn2.MatchDataHandler = func(_ context.Context, msg *MatchDataMsg) { 165 | dataCh <- msg 166 | } 167 | m2, err := conn2.MatchJoin(ctx, m1.MatchId, nil) 168 | switch { 169 | case err != nil: 170 | t.Fatalf("expected no error, got: %v", err) 171 | case m2.MatchId == "": 172 | t.Fatalf("expected non-empty m2.MatchId") 173 | case m1.MatchId != m2.MatchId: 174 | t.Errorf("expected m1.MatchId == m2.MatchId") 175 | case m2.Authoritative: 176 | t.Errorf("expected m2.Authoritative == false") 177 | } 178 | a2, err := cl2.Account(ctx) 179 | if err != nil { 180 | t.Fatalf("expected no error, got: %v", err) 181 | } 182 | t.Logf("account2: %+v", a2) 183 | select { 184 | case <-ctx.Done(): 185 | t.Fatalf("context closed: %v", ctx.Err()) 186 | case msg := <-joinCh: 187 | switch { 188 | case len(msg.Joins) != 1: 189 | t.Fatalf("expected 1 join, got: %d", len(msg.Joins)) 190 | case msg.Joins[0].UserId != a2.User.Id: 191 | // t.Logf("msg: %+v", msg) 192 | // t.Fatalf("expected msg.Joins[0].UserId (%s) == a2.User.Id (%s)", msg.Joins[0].UserId, a2.User.Id) 193 | } 194 | } 195 | if err := conn1.MatchDataSend(ctx, m1.MatchId, 1, []byte(`hello world`), true); err != nil { 196 | t.Fatalf("expected no error, got: %v", err) 197 | } 198 | select { 199 | case <-ctx.Done(): 200 | t.Fatalf("context closed: %v", ctx.Err()) 201 | case msg := <-dataCh: 202 | if s, exp := string(msg.Data), "hello world"; s != exp { 203 | t.Errorf("expected %q, got: %q", exp, s) 204 | } 205 | } 206 | if err := conn1.MatchLeave(ctx, m1.MatchId); err != nil { 207 | t.Fatalf("expected no error, got: %v", err) 208 | } 209 | if err := conn2.MatchLeave(ctx, m2.MatchId); err != nil { 210 | t.Fatalf("expected no error, got: %v", err) 211 | } 212 | } 213 | 214 | func TestRpcRealtime(t *testing.T) { 215 | // TODO 216 | } 217 | 218 | func TestChannels(t *testing.T) { 219 | const target = "my_room" 220 | ctx, cancel, nk := nktest.WithCancel(context.Background(), t) 221 | defer cancel() 222 | cl1 := newClient(ctx, t, nk) 223 | conn1 := createAccountAndConn(ctx, t, cl1, true) 224 | defer conn1.Close() 225 | cl2 := newClient(ctx, t, nk) 226 | conn2 := createAccountAndConn(ctx, t, cl2, true) 227 | defer conn2.Close() 228 | recv := make(chan *ChannelMessage) 229 | conn2.ChannelMessageHandler = func(ctx context.Context, msg *ChannelMessage) { 230 | recv <- msg 231 | } 232 | ch1, err := conn1.ChannelJoin(ctx, target, ChannelType_ROOM, true, false) 233 | if err != nil { 234 | t.Fatalf("expected no error, got: %v", err) 235 | } 236 | t.Logf("created channel: %s", ch1.Id) 237 | if _, err := conn2.ChannelJoin(ctx, target, ChannelType_ROOM, true, false); err != nil { 238 | t.Fatalf("expected no error, got: %v", err) 239 | } 240 | msg := map[string]interface{}{ 241 | "msg": "hello", 242 | "code": float64(15), 243 | } 244 | if _, err := conn1.ChannelMessageSend(ctx, ch1.Id, msg); err != nil { 245 | t.Fatalf("expected no error, got: %v", err) 246 | } 247 | var recvMsg *ChannelMessage 248 | select { 249 | case <-ctx.Done(): 250 | t.Errorf("did not receive message: %v", ctx.Err()) 251 | case <-time.After(1 * time.Minute): 252 | t.Error("did not receive message: timeout hit") 253 | case recvMsg = <-recv: 254 | t.Logf("received: %v", msg) 255 | } 256 | var m map[string]interface{} 257 | if err := json.Unmarshal([]byte(recvMsg.Content), &m); err != nil { 258 | t.Fatalf("expected no error, got: %v", err) 259 | } 260 | if !reflect.DeepEqual(m, msg) { 261 | t.Errorf("expected m == msg:\n%#v\ngot:\n%#v", msg, m) 262 | } 263 | } 264 | 265 | func TestPersist(t *testing.T) { 266 | ctx, cancel, nk := nktest.WithCancel(context.Background(), t) 267 | cl := newClient(ctx, t, nk) 268 | conn := createAccountAndConn(ctx, t, cl, false, WithConnPersist(true)) 269 | <-time.After(2 * conn.backoffMin) 270 | if conn.stop == true { 271 | t.Errorf("expected conn.stop == false") 272 | } 273 | if conn.Connected() == false { 274 | t.Fatalf("expected conn.Connected() == true") 275 | } 276 | if err := conn.Close(); err != nil { 277 | t.Fatalf("expected on error, got: %v", err) 278 | } 279 | if conn.stop == false { 280 | t.Errorf("expected conn.stop == true") 281 | } 282 | if conn.Connected() == true { 283 | t.Errorf("expected conn.Connected() == false") 284 | } 285 | connectCh := make(chan bool, 1) 286 | conn.ConnectHandler = func(context.Context) { 287 | t.Logf("connected") 288 | connectCh <- true 289 | } 290 | disconnectCh := make(chan error, 1) 291 | conn.DisconnectHandler = func(_ context.Context, err error) { 292 | t.Logf("disconnected: %v", err) 293 | disconnectCh <- err 294 | } 295 | if err := conn.Open(ctx); err != nil { 296 | t.Fatalf("expected no error, got: %v", err) 297 | } 298 | <-time.After(2 * conn.backoffMin) 299 | if conn.stop == true { 300 | t.Errorf("expected conn.stop == false") 301 | } 302 | if conn.Connected() == false { 303 | t.Fatalf("expected conn.Connected() == true") 304 | } 305 | select { 306 | case <-ctx.Done(): 307 | t.Fatalf("expected no error, got: %v", ctx.Err()) 308 | case <-time.After(4 * conn.backoffMin): 309 | t.Fatalf("expected a connect event within %v", 4*conn.backoffMin) 310 | case b := <-connectCh: 311 | if b == false { 312 | t.Errorf("expected true") 313 | } 314 | t.Logf("connected: %t", b) 315 | } 316 | if err := conn.CloseWithStopErr(false, false, nil); err != nil { 317 | t.Fatalf("expected no error, got: %v", err) 318 | } 319 | if conn.stop == true { 320 | t.Errorf("expected conn.stop == false") 321 | } 322 | <-time.After(4 * conn.backoffMin) 323 | if conn.Connected() == false { 324 | t.Errorf("expected conn.Connected() == true") 325 | } 326 | select { 327 | case <-ctx.Done(): 328 | t.Fatalf("expected no error, got: %v", ctx.Err()) 329 | case err := <-disconnectCh: 330 | if err != nil { 331 | t.Logf("disconnected!") 332 | t.Fatalf("expected no error, got: %v", err) 333 | } 334 | case <-time.After(conn.backoffMax): 335 | t.Errorf("expected a disconnect event within %v", conn.backoffMax) 336 | } 337 | // check no disconnect event received 338 | select { 339 | case <-ctx.Done(): 340 | t.Errorf("expected no error, got: %v", ctx.Err()) 341 | return 342 | case err := <-disconnectCh: 343 | t.Errorf("expected no disconnect event, got: %v", err) 344 | case <-time.After(conn.backoffMax): 345 | t.Logf("no disconnect") 346 | } 347 | if err := conn.CloseWithStopErr(true, true, errors.New("STOPPING")); err != nil { 348 | t.Fatalf("expected no error, got: %v", err) 349 | } 350 | if conn.stop == false { 351 | t.Errorf("expected conn.stop == true") 352 | } 353 | <-time.After(4 * conn.backoffMin) 354 | if conn.Connected() == true { 355 | t.Errorf("expected conn.Connected() == false") 356 | } 357 | select { 358 | case <-ctx.Done(): 359 | t.Fatalf("expected no error, got: %v", ctx.Err()) 360 | case err := <-disconnectCh: 361 | switch { 362 | case err == nil: 363 | t.Error("expected disconnect event!") 364 | case err.Error() != "STOPPING": 365 | t.Error("expected STOPPING error") 366 | } 367 | } 368 | switch { 369 | case conn.Connected() == true: 370 | t.Errorf("expceted conn.Connected() == false") 371 | case conn.stop == false: 372 | t.Errorf("expected conn.stop == true") 373 | } 374 | cancel() 375 | <-time.After(conn.backoffMax) 376 | /* 377 | close(connectCh) 378 | close(disconnectCh) 379 | */ 380 | } 381 | 382 | func TestKeep(t *testing.T) { 383 | keep := os.Getenv("KEEP") 384 | if keep == "" { 385 | return 386 | } 387 | d, err := time.ParseDuration(keep) 388 | if err != nil { 389 | t.Fatalf("expected no error, got: %v", err) 390 | } 391 | ctx, cancel, nk := nktest.WithCancel(context.Background(), t) 392 | defer cancel() 393 | urlstr, err := nk.RunProxy(ctx) 394 | if err != nil { 395 | t.Fatalf("expected no error, got: %v", err) 396 | } 397 | t.Logf("local: %s", nk.HttpLocal()) 398 | t.Logf("grpc: %s", nk.GrpcLocal()) 399 | t.Logf("http: %s", nk.HttpLocal()) 400 | t.Logf("console: %s", nk.ConsoleLocal()) 401 | t.Logf("http_key: %s", nk.HttpKey()) 402 | t.Logf("server_key: %s", nk.ServerKey()) 403 | t.Logf("proxy: %s", urlstr) 404 | select { 405 | case <-time.After(d): 406 | case <-ctx.Done(): 407 | } 408 | } 409 | 410 | func newClient(ctx context.Context, t *testing.T, nk *nktest.Runner, opts ...Option) *Client { 411 | urlstr, err := nktest.RunProxy(ctx) 412 | if err != nil { 413 | t.Fatalf("expected no error, got: %v", err) 414 | } 415 | t.Logf("url: %s", urlstr) 416 | opts = append([]Option{ 417 | WithURL(urlstr), 418 | WithServerKey(nk.ServerKey()), 419 | WithTransport(&http.Transport{ 420 | DisableCompression: true, 421 | }), 422 | }, opts...) 423 | return New(opts...) 424 | } 425 | 426 | func createAccount(ctx context.Context, t *testing.T, cl *Client) { 427 | deviceId := uuid.New().String() 428 | t.Logf("registering: %s", deviceId) 429 | if err := cl.AuthenticateDevice(ctx, deviceId, true, ""); err != nil { 430 | t.Fatalf("expected no error: got: %v", err) 431 | } 432 | expiry := cl.SessionExpiry() 433 | t.Logf("expiry: %s", cl.SessionExpiry()) 434 | if expiry.IsZero() || expiry.Before(time.Now()) { 435 | t.Fatalf("expected non-zero expiry in the future, got: %s", expiry) 436 | } 437 | res, err := cl.Account(ctx) 438 | if err != nil { 439 | t.Fatalf("expected no error, got: %v", err) 440 | } 441 | t.Logf("account: %+v", res) 442 | if len(res.Devices) == 0 { 443 | t.Fatalf("expected there to be at least one device") 444 | } 445 | found := false 446 | for _, d := range res.Devices { 447 | if d.Id == deviceId { 448 | found = true 449 | break 450 | } 451 | } 452 | if !found { 453 | t.Fatalf("expected accountRes.Devices to contain %s", deviceId) 454 | } 455 | } 456 | 457 | func createAccountAndConn(ctx context.Context, t *testing.T, cl *Client, check bool, opts ...ConnOption) *Conn { 458 | createAccount(ctx, t, cl) 459 | conn, err := cl.NewConn(ctx, append([]ConnOption{WithConnFormat("json")}, opts...)...) 460 | if err != nil { 461 | t.Fatalf("expected no error, got: %v", err) 462 | } 463 | if check && conn.Connected() != true { 464 | t.Fatalf("expected conn.Connected() == true") 465 | } 466 | return conn 467 | } 468 | 469 | type rewards struct { 470 | Rewards int64 `json:"rewards,omitempty"` 471 | } 472 | -------------------------------------------------------------------------------- /realtime.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Nakama Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * The realtime protocol for Nakama server. 17 | */ 18 | syntax = "proto3"; 19 | 20 | package nakama; 21 | 22 | import "google/protobuf/timestamp.proto"; 23 | import "google/protobuf/wrappers.proto"; 24 | import "nakama.proto"; 25 | 26 | option go_package = "github.com/ascii8/nakama-go;nakama"; 27 | 28 | option java_multiple_files = true; 29 | option java_outer_classname = "NakamaRealtime"; 30 | option java_package = "com.heroiclabs.nakama.rtapi"; 31 | 32 | option csharp_namespace = "Nakama.Protobuf"; 33 | 34 | // An envelope for a realtime message. 35 | message Envelope { 36 | string cid = 1; 37 | oneof message { 38 | // A response from a channel join operation. 39 | ChannelMsg channel = 2; 40 | // Join a realtime chat channel. 41 | ChannelJoinMsg channel_join = 3; 42 | // Leave a realtime chat channel. 43 | ChannelLeaveMsg channel_leave = 4; 44 | // An incoming message on a realtime chat channel. 45 | ChannelMessage channel_message = 5; 46 | // An acknowledgement received in response to sending a message on a chat channel. 47 | ChannelMessageAckMsg channel_message_ack = 6; 48 | // Send a message to a realtime chat channel. 49 | ChannelMessageSendMsg channel_message_send = 7; 50 | // Update a message previously sent to a realtime chat channel. 51 | ChannelMessageUpdateMsg channel_message_update = 8; 52 | // Remove a message previously sent to a realtime chat channel. 53 | ChannelMessageRemoveMsg channel_message_remove = 9; 54 | // Presence update for a particular realtime chat channel. 55 | ChannelPresenceEventMsg channel_presence_event = 10; 56 | // Describes an error which occurred on the server. 57 | ErrorMsg error = 11; 58 | // Incoming information about a realtime match. 59 | MatchMsg match = 12; 60 | // A client to server request to create a realtime match. 61 | MatchCreateMsg match_create = 13; 62 | // Incoming realtime match data delivered from the server. 63 | MatchDataMsg match_data = 14; 64 | // A client to server request to send data to a realtime match. 65 | MatchDataSendMsg match_data_send = 15; 66 | // A client to server request to join a realtime match. 67 | MatchJoinMsg match_join = 16; 68 | // A client to server request to leave a realtime match. 69 | MatchLeaveMsg match_leave = 17; 70 | // Presence update for a particular realtime match. 71 | MatchPresenceEventMsg match_presence_event = 18; 72 | // Submit a new matchmaking process request. 73 | MatchmakerAddMsg matchmaker_add = 19; 74 | // A successful matchmaking result. 75 | MatchmakerMatchedMsg matchmaker_matched = 20; 76 | // Cancel a matchmaking process using a ticket. 77 | MatchmakerRemoveMsg matchmaker_remove = 21; 78 | // A response from starting a new matchmaking process. 79 | MatchmakerTicketMsg matchmaker_ticket = 22; 80 | // Notifications send by the server. 81 | NotificationsMsg notifications = 23; 82 | // RPC call or response. 83 | RpcMsg rpc = 24; 84 | // An incoming status snapshot for some set of users. 85 | StatusMsg status = 25; 86 | // Start following some set of users to receive their status updates. 87 | StatusFollowMsg status_follow = 26; 88 | // An incoming status update. 89 | StatusPresenceEventMsg status_presence_event = 27; 90 | // Stop following some set of users to no longer receive their status updates. 91 | StatusUnfollowMsg status_unfollow = 28; 92 | // Set the user's own status. 93 | StatusUpdateMsg status_update = 29; 94 | // A data message delivered over a stream. 95 | StreamDataMsg stream_data = 30; 96 | // Presence update for a particular stream. 97 | StreamPresenceEventMsg stream_presence_event = 31; 98 | // Application-level heartbeat and connection check. 99 | PingMsg ping = 32; 100 | // Application-level heartbeat and connection check response. 101 | PongMsg pong = 33; 102 | // Incoming information about a party. 103 | PartyMsg party = 34; 104 | // Create a party. 105 | PartyCreateMsg party_create = 35; 106 | // Join a party, or request to join if the party is not open. 107 | PartyJoinMsg party_join = 36; 108 | // Leave a party. 109 | PartyLeaveMsg party_leave = 37; 110 | // Promote a new party leader. 111 | PartyPromoteMsg party_promote = 38; 112 | // Announcement of a new party leader. 113 | PartyLeaderMsg party_leader = 39; 114 | // Accept a request to join. 115 | PartyAcceptMsg party_accept = 40; 116 | // Kick a party member, or decline a request to join. 117 | PartyRemoveMsg party_remove = 41; 118 | // End a party, kicking all party members and closing it. 119 | PartyCloseMsg party_close = 42; 120 | // Request a list of pending join requests for a party. 121 | PartyJoinRequestsMsg party_join_request_list = 43; 122 | // Incoming notification for one or more new presences attempting to join the party. 123 | PartyJoinRequestMsg party_join_request = 44; 124 | // Begin matchmaking as a party. 125 | PartyMatchmakerAddMsg party_matchmaker_add = 45; 126 | // Cancel a party matchmaking process using a ticket. 127 | PartyMatchmakerRemoveMsg party_matchmaker_remove = 46; 128 | // A response from starting a new party matchmaking process. 129 | PartyMatchmakerTicketMsg party_matchmaker_ticket = 47; 130 | // Incoming party data delivered from the server. 131 | PartyDataMsg party_data = 48; 132 | // A client to server request to send data to a party. 133 | PartyDataSendMsg party_data_send = 49; 134 | // Presence update for a particular party. 135 | PartyPresenceEventMsg party_presence_event = 50; 136 | } 137 | } 138 | 139 | // A realtime chat channel. 140 | message ChannelMsg { 141 | // The ID of the channel. 142 | string id = 1; 143 | // The users currently in the channel. 144 | repeated UserPresenceMsg presences = 2; 145 | // A reference to the current user's presence in the channel. 146 | UserPresenceMsg self = 3; 147 | // The name of the chat room, or an empty string if this message was not sent through a chat room. 148 | string room_name = 4; 149 | // The ID of the group, or an empty string if this message was not sent through a group channel. 150 | string group_id = 5; 151 | // The ID of the first DM user, or an empty string if this message was not sent through a DM chat. 152 | string user_id_one = 6; 153 | // The ID of the second DM user, or an empty string if this message was not sent through a DM chat. 154 | string user_id_two = 7; 155 | } 156 | 157 | // The type of chat channel. 158 | enum ChannelType { 159 | // Default case. Assumed as ROOM type. 160 | TYPE_UNSPECIFIED = 0; 161 | // A room which anyone can join to chat. 162 | ROOM = 1; 163 | // A private channel for 1-on-1 chat. 164 | DIRECT_MESSAGE = 2; 165 | // A channel for group chat. 166 | GROUP = 3; 167 | } 168 | 169 | // Join operation for a realtime chat channel. 170 | message ChannelJoinMsg { 171 | // The user ID to DM with, group ID to chat with, or room channel name to join. 172 | string target = 1; 173 | // The type of the chat channel. 174 | int32 type = 2; // one of "ChannelId.Type". 175 | // Whether messages sent on this channel should be persistent. 176 | google.protobuf.BoolValue persistence = 3; 177 | // Whether the user should appear in the channel's presence list and events. 178 | google.protobuf.BoolValue hidden = 4; 179 | } 180 | 181 | // Leave a realtime channel. 182 | message ChannelLeaveMsg { 183 | // The ID of the channel to leave. 184 | string channel_id = 1; 185 | } 186 | 187 | // A receipt reply from a channel message send operation. 188 | message ChannelMessageAckMsg { 189 | // The channel the message was sent to. 190 | string channel_id = 1; 191 | // The unique ID assigned to the message. 192 | string message_id = 2; 193 | // The code representing a message type or category. 194 | google.protobuf.Int32Value code = 3; 195 | // Username of the message sender. 196 | string username = 4; 197 | // The UNIX time (for gRPC clients) or ISO string (for REST clients) when the message was created. 198 | google.protobuf.Timestamp create_time = 5; 199 | // The UNIX time (for gRPC clients) or ISO string (for REST clients) when the message was created. 200 | google.protobuf.Timestamp update_time = 6; 201 | // True if the message was persisted to the channel's history, false otherwise. 202 | google.protobuf.BoolValue persistent = 7; 203 | // The name of the chat room, or an empty string if this message was not sent through a chat room. 204 | string room_name = 8; 205 | // The ID of the group, or an empty string if this message was not sent through a group channel. 206 | string group_id = 9; 207 | // The ID of the first DM user, or an empty string if this message was not sent through a DM chat. 208 | string user_id_one = 10; 209 | // The ID of the second DM user, or an empty string if this message was not sent through a DM chat. 210 | string user_id_two = 11; 211 | } 212 | 213 | // Send a message to a realtime channel. 214 | message ChannelMessageSendMsg { 215 | // The channel to sent to. 216 | string channel_id = 1; 217 | // Message content. 218 | string content = 2; 219 | } 220 | 221 | // Update a message previously sent to a realtime channel. 222 | message ChannelMessageUpdateMsg { 223 | // The channel the message was sent to. 224 | string channel_id = 1; 225 | // The ID assigned to the message to update. 226 | string message_id = 2; 227 | // New message content. 228 | string content = 3; 229 | } 230 | 231 | // Remove a message previously sent to a realtime channel. 232 | message ChannelMessageRemoveMsg { 233 | // The channel the message was sent to. 234 | string channel_id = 1; 235 | // The ID assigned to the message to update. 236 | string message_id = 2; 237 | } 238 | 239 | // A set of joins and leaves on a particular channel. 240 | message ChannelPresenceEventMsg { 241 | // The channel identifier this event is for. 242 | string channel_id = 1; 243 | // Presences joining the channel as part of this event, if any. 244 | repeated UserPresenceMsg joins = 2; 245 | // Presences leaving the channel as part of this event, if any. 246 | repeated UserPresenceMsg leaves = 3; 247 | // The name of the chat room, or an empty string if this message was not sent through a chat room. 248 | string room_name = 4; 249 | // The ID of the group, or an empty string if this message was not sent through a group channel. 250 | string group_id = 5; 251 | // The ID of the first DM user, or an empty string if this message was not sent through a DM chat. 252 | string user_id_one = 6; 253 | // The ID of the second DM user, or an empty string if this message was not sent through a DM chat. 254 | string user_id_two = 7; 255 | } 256 | 257 | // The selection of possible error codes. 258 | enum ErrorCode { 259 | // An unexpected result from the server. 260 | RUNTIME_EXCEPTION = 0; 261 | // The server received a message which is not recognised. 262 | UNRECOGNIZED_PAYLOAD = 1; 263 | // A message was expected but contains no content. 264 | MISSING_PAYLOAD = 2; 265 | // Fields in the message have an invalid format. 266 | BAD_INPUT = 3; 267 | // The match id was not found. 268 | MATCH_NOT_FOUND = 4; 269 | // The match join was rejected. 270 | MATCH_JOIN_REJECTED = 5; 271 | // The runtime function does not exist on the server. 272 | RUNTIME_FUNCTION_NOT_FOUND = 6; 273 | // The runtime function executed with an error. 274 | RUNTIME_FUNCTION_EXCEPTION = 7; 275 | } 276 | 277 | // A logical error which may occur on the server. 278 | message ErrorMsg { 279 | // The error code which should be one of "Error.Code" enums. 280 | int32 code = 1; 281 | // A message in English to help developers debug the response. 282 | string message = 2; 283 | // Additional error details which may be different for each response. 284 | map context = 3; 285 | } 286 | 287 | // A realtime match. 288 | message MatchMsg { 289 | // The match unique ID. 290 | string match_id = 1; 291 | // True if it's an server-managed authoritative match, false otherwise. 292 | bool authoritative = 2; 293 | // Match label, if any. 294 | google.protobuf.StringValue label = 3; 295 | // The number of users currently in the match. 296 | int32 size = 4; 297 | // The users currently in the match. 298 | repeated UserPresenceMsg presences = 5; 299 | // A reference to the current user's presence in the match. 300 | UserPresenceMsg self = 6; 301 | } 302 | 303 | // Create a new realtime match. 304 | message MatchCreateMsg { 305 | // Optional name to use when creating the match. 306 | string name = 1; 307 | } 308 | 309 | // Realtime match data received from the server. 310 | message MatchDataMsg { 311 | // The match unique ID. 312 | string match_id = 1; 313 | // A reference to the user presence that sent this data, if any. 314 | UserPresenceMsg presence = 2; 315 | // Op code value. 316 | int64 op_code = 3; 317 | // Data payload, if any. 318 | bytes data = 4; 319 | // True if this data was delivered reliably, false otherwise. 320 | bool reliable = 5; 321 | } 322 | 323 | // Send realtime match data to the server. 324 | message MatchDataSendMsg { 325 | // The match unique ID. 326 | string match_id = 1; 327 | // Op code value. 328 | int64 op_code = 2; 329 | // Data payload, if any. 330 | bytes data = 3; 331 | // List of presences in the match to deliver to, if filtering is required. Otherwise deliver to everyone in the match. 332 | repeated UserPresenceMsg presences = 4; 333 | // True if the data should be sent reliably, false otherwise. 334 | bool reliable = 5; 335 | } 336 | 337 | // Join an existing realtime match. 338 | message MatchJoinMsg { 339 | oneof id { 340 | // The match unique ID. 341 | string match_id = 1; 342 | // A matchmaking result token. 343 | string token = 2; 344 | } 345 | // An optional set of key-value metadata pairs to be passed to the match handler, if any. 346 | map metadata = 3; 347 | } 348 | 349 | // Leave a realtime match. 350 | message MatchLeaveMsg { 351 | // The match unique ID. 352 | string match_id = 1; 353 | } 354 | 355 | // A set of joins and leaves on a particular realtime match. 356 | message MatchPresenceEventMsg { 357 | // The match unique ID. 358 | string match_id = 1; 359 | // User presences that have just joined the match. 360 | repeated UserPresenceMsg joins = 2; 361 | // User presences that have just left the match. 362 | repeated UserPresenceMsg leaves = 3; 363 | } 364 | 365 | // Start a new matchmaking process. 366 | message MatchmakerAddMsg { 367 | // Minimum total user count to match together. 368 | int32 min_count = 1; 369 | // Maximum total user count to match together. 370 | int32 max_count = 2; 371 | // Filter query used to identify suitable users. 372 | string query = 3; 373 | // String properties. 374 | map string_properties = 4; 375 | // Numeric properties. 376 | map numeric_properties = 5; 377 | // Optional multiple of the count that must be satisfied. 378 | google.protobuf.Int32Value count_multiple = 6; 379 | } 380 | 381 | message MatchmakerUserMsg { 382 | // User info. 383 | UserPresenceMsg presence = 1; 384 | // Party identifier, if this user was matched as a party member. 385 | string party_id = 2; 386 | // String properties. 387 | map string_properties = 5; 388 | // Numeric properties. 389 | map numeric_properties = 6; 390 | } 391 | 392 | // A successful matchmaking result. 393 | message MatchmakerMatchedMsg { 394 | // The matchmaking ticket that has completed. 395 | string ticket = 1; 396 | // The match token or match ID to join. 397 | oneof id { 398 | // Match ID. 399 | string match_id = 2; 400 | // Match join token. 401 | string token = 3; 402 | } 403 | // The users that have been matched together, and information about their matchmaking data. 404 | repeated MatchmakerUserMsg users = 4; 405 | // A reference to the current user and their properties. 406 | MatchmakerUserMsg self = 5; 407 | } 408 | 409 | // Cancel an existing ongoing matchmaking process. 410 | message MatchmakerRemoveMsg { 411 | // The ticket to cancel. 412 | string ticket = 1; 413 | } 414 | 415 | // A ticket representing a new matchmaking process. 416 | message MatchmakerTicketMsg { 417 | // The ticket that can be used to cancel matchmaking. 418 | string ticket = 1; 419 | } 420 | 421 | // A collection of zero or more notifications. 422 | message NotificationsMsg { 423 | // Collection of notifications. 424 | repeated Notification notifications = 1; 425 | } 426 | 427 | // Incoming information about a party. 428 | message PartyMsg { 429 | // Unique party identifier. 430 | string party_id = 1; 431 | // Open flag. 432 | bool open = 2; 433 | // Maximum number of party members. 434 | int32 max_size = 3; 435 | // Self. 436 | UserPresenceMsg self = 4; 437 | // Leader. 438 | UserPresenceMsg leader = 5; 439 | // All current party members. 440 | repeated UserPresenceMsg presences = 6; 441 | } 442 | 443 | // Create a party. 444 | message PartyCreateMsg { 445 | // Whether or not the party will require join requests to be approved by the party leader. 446 | bool open = 1; 447 | // Maximum number of party members. 448 | int32 max_size = 2; 449 | } 450 | 451 | // Join a party, or request to join if the party is not open. 452 | message PartyJoinMsg { 453 | // Party ID to join. 454 | string party_id = 1; 455 | } 456 | 457 | // Leave a party. 458 | message PartyLeaveMsg { 459 | // Party ID to leave. 460 | string party_id = 1; 461 | } 462 | 463 | // Promote a new party leader. 464 | message PartyPromoteMsg { 465 | // Party ID to promote a new leader for. 466 | string party_id = 1; 467 | // The presence of an existing party member to promote as the new leader. 468 | UserPresenceMsg presence = 2; 469 | } 470 | 471 | // Announcement of a new party leader. 472 | message PartyLeaderMsg { 473 | // Party ID to announce the new leader for. 474 | string party_id = 1; 475 | // The presence of the new party leader. 476 | UserPresenceMsg presence = 2; 477 | } 478 | 479 | // Accept a request to join. 480 | message PartyAcceptMsg { 481 | // Party ID to accept a join request for. 482 | string party_id = 1; 483 | // The presence to accept as a party member. 484 | UserPresenceMsg presence = 2; 485 | } 486 | 487 | // Kick a party member, or decline a request to join. 488 | message PartyRemoveMsg { 489 | // Party ID to remove/reject from. 490 | string party_id = 1; 491 | // The presence to remove or reject. 492 | UserPresenceMsg presence = 2; 493 | } 494 | 495 | // End a party, kicking all party members and closing it. 496 | message PartyCloseMsg { 497 | // Party ID to close. 498 | string party_id = 1; 499 | } 500 | 501 | // Request a list of pending join requests for a party. 502 | message PartyJoinRequestsMsg { 503 | // Party ID to get a list of join requests for. 504 | string party_id = 1; 505 | } 506 | 507 | // Incoming notification for one or more new presences attempting to join the party. 508 | message PartyJoinRequestMsg { 509 | // Party ID these presences are attempting to join. 510 | string party_id = 1; 511 | // Presences attempting to join. 512 | repeated UserPresenceMsg presences = 2; 513 | } 514 | 515 | // Begin matchmaking as a party. 516 | message PartyMatchmakerAddMsg { 517 | // Party ID. 518 | string party_id = 1; 519 | // Minimum total user count to match together. 520 | int32 min_count = 2; 521 | // Maximum total user count to match together. 522 | int32 max_count = 3; 523 | // Filter query used to identify suitable users. 524 | string query = 4; 525 | // String properties. 526 | map string_properties = 5; 527 | // Numeric properties. 528 | map numeric_properties = 6; 529 | // Optional multiple of the count that must be satisfied. 530 | google.protobuf.Int32Value count_multiple = 7; 531 | } 532 | 533 | // Cancel a party matchmaking process using a ticket. 534 | message PartyMatchmakerRemoveMsg { 535 | // Party ID. 536 | string party_id = 1; 537 | // The ticket to cancel. 538 | string ticket = 2; 539 | } 540 | 541 | // A response from starting a new party matchmaking process. 542 | message PartyMatchmakerTicketMsg { 543 | // Party ID. 544 | string party_id = 1; 545 | // The ticket that can be used to cancel matchmaking. 546 | string ticket = 2; 547 | } 548 | 549 | // Incoming party data delivered from the server. 550 | message PartyDataMsg { 551 | // The party ID. 552 | string party_id = 1; 553 | // A reference to the user presence that sent this data, if any. 554 | UserPresenceMsg presence = 2; 555 | // Op code value. 556 | int64 op_code = 3; 557 | // Data payload, if any. 558 | bytes data = 4; 559 | } 560 | 561 | // Send data to a party. 562 | message PartyDataSendMsg { 563 | // Party ID to send to. 564 | string party_id = 1; 565 | // Op code value. 566 | int64 op_code = 2; 567 | // Data payload, if any. 568 | bytes data = 3; 569 | } 570 | 571 | // Presence update for a particular party. 572 | message PartyPresenceEventMsg { 573 | // The party ID. 574 | string party_id = 1; 575 | // User presences that have just joined the party. 576 | repeated UserPresenceMsg joins = 2; 577 | // User presences that have just left the party. 578 | repeated UserPresenceMsg leaves = 3; 579 | } 580 | 581 | 582 | // Application-level heartbeat and connection check. 583 | message PingMsg {} 584 | 585 | // Application-level heartbeat and connection check response. 586 | message PongMsg {} 587 | 588 | // A snapshot of statuses for some set of users. 589 | message StatusMsg { 590 | // User statuses. 591 | repeated UserPresenceMsg presences = 1; 592 | } 593 | 594 | // Start receiving status updates for some set of users. 595 | message StatusFollowMsg { 596 | // User IDs to follow. 597 | repeated string user_ids = 1; 598 | // Usernames to follow. 599 | repeated string usernames = 2; 600 | } 601 | 602 | // A batch of status updates for a given user. 603 | message StatusPresenceEventMsg { 604 | // New statuses for the user. 605 | repeated UserPresenceMsg joins = 2; 606 | // Previous statuses for the user. 607 | repeated UserPresenceMsg leaves = 3; 608 | } 609 | 610 | // Stop receiving status updates for some set of users. 611 | message StatusUnfollowMsg { 612 | // Users to unfollow. 613 | repeated string user_ids = 1; 614 | } 615 | 616 | // Set the user's own status. 617 | message StatusUpdateMsg { 618 | // Status string to set, if not present the user will appear offline. 619 | google.protobuf.StringValue status = 1; 620 | } 621 | 622 | // Represents identifying information for a stream. 623 | message StreamMsg { 624 | // Mode identifies the type of stream. 625 | int32 mode = 1; 626 | // Subject is the primary identifier, if any. 627 | string subject = 2; 628 | // Subcontext is a secondary identifier, if any. 629 | string subcontext = 3; 630 | // The label is an arbitrary identifying string, if the stream has one. 631 | string label = 4; 632 | } 633 | 634 | // A data message delivered over a stream. 635 | message StreamDataMsg { 636 | // The stream this data message relates to. 637 | StreamMsg stream = 1; 638 | // The sender, if any. 639 | UserPresenceMsg sender = 2; 640 | // Arbitrary contents of the data message. 641 | string data = 3; 642 | // True if this data was delivered reliably, false otherwise. 643 | bool reliable = 4; 644 | } 645 | 646 | // A set of joins and leaves on a particular stream. 647 | message StreamPresenceEventMsg { 648 | // The stream this event relates to. 649 | StreamMsg stream = 1; 650 | // Presences joining the stream as part of this event, if any. 651 | repeated UserPresenceMsg joins = 2; 652 | // Presences leaving the stream as part of this event, if any. 653 | repeated UserPresenceMsg leaves = 3; 654 | } 655 | 656 | // A user session associated to a stream, usually through a list operation or a join/leave event. 657 | message UserPresenceMsg { 658 | // The user this presence belongs to. 659 | string user_id = 1; 660 | // A unique session ID identifying the particular connection, because the user may have many. 661 | string session_id = 2; 662 | // The username for display purposes. 663 | string username = 3; 664 | // Whether this presence generates persistent data/messages, if applicable for the stream type. 665 | bool persistence = 4; 666 | // A user-set status message for this stream, if applicable. 667 | google.protobuf.StringValue status = 5; 668 | } 669 | -------------------------------------------------------------------------------- /realtime.go: -------------------------------------------------------------------------------- 1 | package nakama 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "sort" 8 | "strings" 9 | 10 | "google.golang.org/protobuf/encoding/protojson" 11 | "google.golang.org/protobuf/proto" 12 | "google.golang.org/protobuf/types/known/wrapperspb" 13 | ) 14 | 15 | // EnvelopeBuilder is the shared interface for realtime messages. 16 | type EnvelopeBuilder interface { 17 | BuildEnvelope() *Envelope 18 | } 19 | 20 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 21 | func (msg *ChannelMsg) BuildEnvelope() *Envelope { 22 | return &Envelope{ 23 | Message: &Envelope_Channel{ 24 | Channel: msg, 25 | }, 26 | } 27 | } 28 | 29 | // ChannelJoin creates a realtime message to join a chat channel. 30 | func ChannelJoin(target string, typ ChannelType) *ChannelJoinMsg { 31 | return &ChannelJoinMsg{ 32 | Target: target, 33 | Type: int32(typ), 34 | } 35 | } 36 | 37 | // WithPersistence sets the persistence on the message. 38 | func (msg *ChannelJoinMsg) WithPersistence(persistence bool) *ChannelJoinMsg { 39 | msg.Persistence = wrapperspb.Bool(persistence) 40 | return msg 41 | } 42 | 43 | // WithHidden sets the hidden on the message. 44 | func (msg *ChannelJoinMsg) WithHidden(hidden bool) *ChannelJoinMsg { 45 | msg.Hidden = wrapperspb.Bool(hidden) 46 | return msg 47 | } 48 | 49 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 50 | func (msg *ChannelJoinMsg) BuildEnvelope() *Envelope { 51 | return &Envelope{ 52 | Message: &Envelope_ChannelJoin{ 53 | ChannelJoin: msg, 54 | }, 55 | } 56 | } 57 | 58 | // Send sends the message to the connection. 59 | func (msg *ChannelJoinMsg) Send(ctx context.Context, conn *Conn) (*ChannelMsg, error) { 60 | res := new(ChannelMsg) 61 | if err := conn.Send(ctx, msg, res); err != nil { 62 | return nil, err 63 | } 64 | return res, nil 65 | } 66 | 67 | // Async sends the message to the connection. 68 | func (msg *ChannelJoinMsg) Async(ctx context.Context, conn *Conn, f func(*ChannelMsg, error)) { 69 | go func() { 70 | if res, err := msg.Send(ctx, conn); f != nil { 71 | f(res, err) 72 | } 73 | }() 74 | } 75 | 76 | // ChannelLeave creates a realtime message to leave a chat channel. 77 | func ChannelLeave(channelId string) *ChannelLeaveMsg { 78 | return &ChannelLeaveMsg{ 79 | ChannelId: channelId, 80 | } 81 | } 82 | 83 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 84 | func (msg *ChannelLeaveMsg) BuildEnvelope() *Envelope { 85 | return &Envelope{ 86 | Message: &Envelope_ChannelLeave{ 87 | ChannelLeave: msg, 88 | }, 89 | } 90 | } 91 | 92 | // Send sends the message to the connection. 93 | func (msg *ChannelLeaveMsg) Send(ctx context.Context, conn *Conn) error { 94 | return conn.Send(ctx, msg, empty()) 95 | } 96 | 97 | // Async sends the message to the connection. 98 | func (msg *ChannelLeaveMsg) Async(ctx context.Context, conn *Conn, f func(error)) { 99 | go func() { 100 | if err := msg.Send(ctx, conn); f != nil { 101 | f(err) 102 | } 103 | }() 104 | } 105 | 106 | // ChannelMessageMsg is a realtime channel message message. 107 | type ChannelMessageMsg = ChannelMessage 108 | 109 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 110 | func (msg *ChannelMessageMsg) BuildEnvelope() *Envelope { 111 | return &Envelope{ 112 | Message: &Envelope_ChannelMessage{ 113 | ChannelMessage: msg, 114 | }, 115 | } 116 | } 117 | 118 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 119 | func (msg *ChannelMessageAckMsg) BuildEnvelope() *Envelope { 120 | return &Envelope{ 121 | Message: &Envelope_ChannelMessageAck{ 122 | ChannelMessageAck: msg, 123 | }, 124 | } 125 | } 126 | 127 | // ChannelMessageRemove creates a realtime message to remove a message from a channel. 128 | func ChannelMessageRemove(channelId, messageId string) *ChannelMessageRemoveMsg { 129 | return &ChannelMessageRemoveMsg{ 130 | ChannelId: channelId, 131 | MessageId: messageId, 132 | } 133 | } 134 | 135 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 136 | func (msg *ChannelMessageRemoveMsg) BuildEnvelope() *Envelope { 137 | return &Envelope{ 138 | Message: &Envelope_ChannelMessageRemove{ 139 | ChannelMessageRemove: msg, 140 | }, 141 | } 142 | } 143 | 144 | // Send sends the message to the connection. 145 | func (msg *ChannelMessageRemoveMsg) Send(ctx context.Context, conn *Conn) (*ChannelMessageAckMsg, error) { 146 | res := new(ChannelMessageAckMsg) 147 | if err := conn.Send(ctx, msg, res); err != nil { 148 | return nil, err 149 | } 150 | return res, nil 151 | } 152 | 153 | // Async sends the message to the connection. 154 | func (msg *ChannelMessageRemoveMsg) Async(ctx context.Context, conn *Conn, f func(*ChannelMessageAckMsg, error)) { 155 | go func() { 156 | if res, err := msg.Send(ctx, conn); f != nil { 157 | f(res, err) 158 | } 159 | }() 160 | } 161 | 162 | // ChannelMessageSend creates a realtime message to send a message on a channel. 163 | func ChannelMessageSend(channelId string, v interface{}) (*ChannelMessageSendMsg, error) { 164 | var buf []byte 165 | var err error 166 | switch m := v.(type) { 167 | case proto.Message: 168 | buf, err = protojson.Marshal(m) 169 | default: 170 | buf, err = json.Marshal(v) 171 | } 172 | if err != nil { 173 | return nil, err 174 | } 175 | return &ChannelMessageSendMsg{ 176 | ChannelId: channelId, 177 | Content: string(buf), 178 | }, nil 179 | } 180 | 181 | // ChannelMessageSendRaw creates a realtime message to send a message on a channel. 182 | func ChannelMessageSendRaw(channelId, content string) *ChannelMessageSendMsg { 183 | return &ChannelMessageSendMsg{ 184 | ChannelId: channelId, 185 | Content: content, 186 | } 187 | } 188 | 189 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 190 | func (msg *ChannelMessageSendMsg) BuildEnvelope() *Envelope { 191 | return &Envelope{ 192 | Message: &Envelope_ChannelMessageSend{ 193 | ChannelMessageSend: msg, 194 | }, 195 | } 196 | } 197 | 198 | // Send sends the message to the connection. 199 | func (msg *ChannelMessageSendMsg) Send(ctx context.Context, conn *Conn) (*ChannelMessageAckMsg, error) { 200 | res := new(ChannelMessageAckMsg) 201 | if err := conn.Send(ctx, msg, res); err != nil { 202 | return nil, err 203 | } 204 | return res, nil 205 | } 206 | 207 | // Async sends the message to the connection. 208 | func (msg *ChannelMessageSendMsg) Async(ctx context.Context, conn *Conn, f func(*ChannelMessageAckMsg, error)) { 209 | go func() { 210 | if res, err := msg.Send(ctx, conn); f != nil { 211 | f(res, err) 212 | } 213 | }() 214 | } 215 | 216 | // ChannelMessageUpdate creates a realtime message to update a message on a channel. 217 | func ChannelMessageUpdate(channelId, messageId string, v interface{}) (*ChannelMessageUpdateMsg, error) { 218 | var buf []byte 219 | var err error 220 | switch m := v.(type) { 221 | case proto.Message: 222 | buf, err = protojson.Marshal(m) 223 | default: 224 | buf, err = json.Marshal(v) 225 | } 226 | if err != nil { 227 | return nil, err 228 | } 229 | return &ChannelMessageUpdateMsg{ 230 | ChannelId: channelId, 231 | MessageId: messageId, 232 | Content: string(buf), 233 | }, err 234 | } 235 | 236 | // ChannelMessageUpdateRaw creates a realtime message to update a message on a channel. 237 | func ChannelMessageUpdateRaw(channelId, messageId, content string) *ChannelMessageUpdateMsg { 238 | return &ChannelMessageUpdateMsg{ 239 | ChannelId: channelId, 240 | MessageId: messageId, 241 | Content: content, 242 | } 243 | } 244 | 245 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 246 | func (msg *ChannelMessageUpdateMsg) BuildEnvelope() *Envelope { 247 | return &Envelope{ 248 | Message: &Envelope_ChannelMessageUpdate{ 249 | ChannelMessageUpdate: msg, 250 | }, 251 | } 252 | } 253 | 254 | // Send sends the message to the connection. 255 | func (msg *ChannelMessageUpdateMsg) Send(ctx context.Context, conn *Conn) (*ChannelMessageAckMsg, error) { 256 | res := new(ChannelMessageAckMsg) 257 | if err := conn.Send(ctx, msg, res); err != nil { 258 | return nil, err 259 | } 260 | return res, nil 261 | } 262 | 263 | // Async sends the message to the connection. 264 | func (msg *ChannelMessageUpdateMsg) Async(ctx context.Context, conn *Conn, f func(*ChannelMessageAckMsg, error)) { 265 | go func() { 266 | if res, err := msg.Send(ctx, conn); f != nil { 267 | f(res, err) 268 | } 269 | }() 270 | } 271 | 272 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 273 | func (msg *ChannelPresenceEventMsg) BuildEnvelope() *Envelope { 274 | return &Envelope{ 275 | Message: &Envelope_ChannelPresenceEvent{ 276 | ChannelPresenceEvent: msg, 277 | }, 278 | } 279 | } 280 | 281 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 282 | func (msg *ErrorMsg) BuildEnvelope() *Envelope { 283 | return &Envelope{ 284 | Message: &Envelope_Error{ 285 | Error: msg, 286 | }, 287 | } 288 | } 289 | 290 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 291 | func (msg *MatchMsg) BuildEnvelope() *Envelope { 292 | return &Envelope{ 293 | Message: &Envelope_Match{ 294 | Match: msg, 295 | }, 296 | } 297 | } 298 | 299 | // Error satisfies the error interface. 300 | func (err *ErrorMsg) Error() string { 301 | var keys []string 302 | for key := range err.Context { 303 | keys = append(keys, key) 304 | } 305 | sort.Strings(keys) 306 | var s []string 307 | for _, k := range keys { 308 | s = append(s, k+":"+err.Context[k]) 309 | } 310 | var extra string 311 | if len(s) != 0 { 312 | extra = " <" + strings.Join(s, " ") + ">" 313 | } 314 | return fmt.Sprintf("realtime socket error %s (%d): %s%s", ErrorCode(err.Code), err.Code, err.Message, extra) 315 | } 316 | 317 | // MatchCreate creates a realtime message to create a multiplayer match. 318 | func MatchCreate(name string) *MatchCreateMsg { 319 | return &MatchCreateMsg{ 320 | Name: name, 321 | } 322 | } 323 | 324 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 325 | func (msg *MatchCreateMsg) BuildEnvelope() *Envelope { 326 | return &Envelope{ 327 | Message: &Envelope_MatchCreate{ 328 | MatchCreate: msg, 329 | }, 330 | } 331 | } 332 | 333 | // Send sends the message to the connection. 334 | func (msg *MatchCreateMsg) Send(ctx context.Context, conn *Conn) (*MatchMsg, error) { 335 | res := new(MatchMsg) 336 | if err := conn.Send(ctx, msg, res); err != nil { 337 | return nil, err 338 | } 339 | return res, nil 340 | } 341 | 342 | // Async sends the message to the connection. 343 | func (msg *MatchCreateMsg) Async(ctx context.Context, conn *Conn, f func(*MatchMsg, error)) { 344 | go func() { 345 | if res, err := msg.Send(ctx, conn); f != nil { 346 | f(res, err) 347 | } 348 | }() 349 | } 350 | 351 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 352 | func (msg *MatchDataMsg) BuildEnvelope() *Envelope { 353 | return &Envelope{ 354 | Message: &Envelope_MatchData{ 355 | MatchData: msg, 356 | }, 357 | } 358 | } 359 | 360 | // MatchDataSend creates a realtime message to send input to a multiplayer match. 361 | func MatchDataSend(matchId string, opCode int64, data []byte) *MatchDataSendMsg { 362 | return &MatchDataSendMsg{ 363 | MatchId: matchId, 364 | OpCode: opCode, 365 | Data: data, 366 | } 367 | } 368 | 369 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 370 | func (msg *MatchDataSendMsg) BuildEnvelope() *Envelope { 371 | return &Envelope{ 372 | Message: &Envelope_MatchDataSend{ 373 | MatchDataSend: msg, 374 | }, 375 | } 376 | } 377 | 378 | // WithPresences sets the presences on the message. 379 | func (msg *MatchDataSendMsg) WithPresences(presences ...*UserPresenceMsg) *MatchDataSendMsg { 380 | msg.Presences = presences 381 | return msg 382 | } 383 | 384 | // WithReliable sets the reliable on the message. 385 | func (msg *MatchDataSendMsg) WithReliable(reliable bool) *MatchDataSendMsg { 386 | msg.Reliable = reliable 387 | return msg 388 | } 389 | 390 | // Send sends the message to the connection. 391 | func (msg *MatchDataSendMsg) Send(ctx context.Context, conn *Conn) error { 392 | return conn.Send(ctx, msg, nil) 393 | } 394 | 395 | // Async sends the message to the connection. 396 | func (msg *MatchDataSendMsg) Async(ctx context.Context, conn *Conn, f func(error)) { 397 | go func() { 398 | if err := msg.Send(ctx, conn); f != nil { 399 | f(err) 400 | } 401 | }() 402 | } 403 | 404 | // MatchJoin creates a realtime message to join a match. 405 | func MatchJoin(matchId string) *MatchJoinMsg { 406 | return &MatchJoinMsg{ 407 | Id: &MatchJoinMsg_MatchId{ 408 | MatchId: matchId, 409 | }, 410 | } 411 | } 412 | 413 | // MatchJoinToken creates a new realtime to join a match with a token. 414 | func MatchJoinToken(token string) *MatchJoinMsg { 415 | return &MatchJoinMsg{ 416 | Id: &MatchJoinMsg_Token{ 417 | Token: token, 418 | }, 419 | } 420 | } 421 | 422 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 423 | func (msg *MatchJoinMsg) BuildEnvelope() *Envelope { 424 | return &Envelope{ 425 | Message: &Envelope_MatchJoin{ 426 | MatchJoin: msg, 427 | }, 428 | } 429 | } 430 | 431 | // WithMetadata sets the metadata on the message. 432 | func (msg *MatchJoinMsg) WithMetadata(metadata map[string]string) *MatchJoinMsg { 433 | msg.Metadata = metadata 434 | return msg 435 | } 436 | 437 | // Send sends the message to the connection. 438 | func (msg *MatchJoinMsg) Send(ctx context.Context, conn *Conn) (*MatchMsg, error) { 439 | res := new(MatchMsg) 440 | if err := conn.Send(ctx, msg, res); err != nil { 441 | return nil, err 442 | } 443 | return res, nil 444 | } 445 | 446 | // Async sends the message to the connection. 447 | func (msg *MatchJoinMsg) Async(ctx context.Context, conn *Conn, f func(*MatchMsg, error)) { 448 | go func() { 449 | if res, err := msg.Send(ctx, conn); f != nil { 450 | f(res, err) 451 | } 452 | }() 453 | } 454 | 455 | // MatchLeave creates a realtime message to leave a multiplayer match. 456 | func MatchLeave(matchId string) *MatchLeaveMsg { 457 | return &MatchLeaveMsg{ 458 | MatchId: matchId, 459 | } 460 | } 461 | 462 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 463 | func (msg *MatchLeaveMsg) BuildEnvelope() *Envelope { 464 | return &Envelope{ 465 | Message: &Envelope_MatchLeave{ 466 | MatchLeave: msg, 467 | }, 468 | } 469 | } 470 | 471 | // Send sends the message to the connection. 472 | func (msg *MatchLeaveMsg) Send(ctx context.Context, conn *Conn) error { 473 | return conn.Send(ctx, msg, empty()) 474 | } 475 | 476 | // Async sends the message to the connection. 477 | func (msg *MatchLeaveMsg) Async(ctx context.Context, conn *Conn, f func(error)) { 478 | go func() { 479 | if err := msg.Send(ctx, conn); f != nil { 480 | f(err) 481 | } 482 | }() 483 | } 484 | 485 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 486 | func (msg *MatchPresenceEventMsg) BuildEnvelope() *Envelope { 487 | return &Envelope{ 488 | Message: &Envelope_MatchPresenceEvent{ 489 | MatchPresenceEvent: msg, 490 | }, 491 | } 492 | } 493 | 494 | // MatchmakerAdd creates a realtime message to join the matchmaker pool and search for opponents on the server. 495 | func MatchmakerAdd(query string, minCount, maxCount int) *MatchmakerAddMsg { 496 | return &MatchmakerAddMsg{ 497 | Query: query, 498 | MinCount: int32(minCount), 499 | MaxCount: int32(maxCount), 500 | } 501 | } 502 | 503 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 504 | func (msg *MatchmakerAddMsg) BuildEnvelope() *Envelope { 505 | return &Envelope{ 506 | Message: &Envelope_MatchmakerAdd{ 507 | MatchmakerAdd: msg, 508 | }, 509 | } 510 | } 511 | 512 | // WithStringProperties sets the stringProperties on the message. 513 | func (msg *MatchmakerAddMsg) WithStringProperties(stringProperties map[string]string) *MatchmakerAddMsg { 514 | msg.StringProperties = stringProperties 515 | return msg 516 | } 517 | 518 | // WithNumericProperties sets the stringProperties on the message. 519 | func (msg *MatchmakerAddMsg) WithNumericProperties(numericProperties map[string]float64) *MatchmakerAddMsg { 520 | msg.NumericProperties = numericProperties 521 | return msg 522 | } 523 | 524 | // WithCountMultiple sets the stringProperties on the message. 525 | func (msg *MatchmakerAddMsg) WithCountMultiple(countMultiple int) *MatchmakerAddMsg { 526 | msg.CountMultiple = wrapperspb.Int32(int32(countMultiple)) 527 | return msg 528 | } 529 | 530 | // Send sends the message to the connection. 531 | func (msg *MatchmakerAddMsg) Send(ctx context.Context, conn *Conn) (*MatchmakerTicketMsg, error) { 532 | res := new(MatchmakerTicketMsg) 533 | if err := conn.Send(ctx, msg, res); err != nil { 534 | return nil, err 535 | } 536 | return res, nil 537 | } 538 | 539 | // Async sends the message to the connection. 540 | func (msg *MatchmakerAddMsg) Async(ctx context.Context, conn *Conn, f func(*MatchmakerTicketMsg, error)) { 541 | go func() { 542 | if res, err := msg.Send(ctx, conn); f != nil { 543 | f(res, err) 544 | } 545 | }() 546 | } 547 | 548 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 549 | func (msg *MatchmakerMatchedMsg) BuildEnvelope() *Envelope { 550 | return &Envelope{ 551 | Message: &Envelope_MatchmakerMatched{ 552 | MatchmakerMatched: msg, 553 | }, 554 | } 555 | } 556 | 557 | // MatchmakerRemove creates a realtime message to leave the matchmaker pool for a ticket. 558 | func MatchmakerRemove(ticket string) *MatchmakerRemoveMsg { 559 | return &MatchmakerRemoveMsg{ 560 | Ticket: ticket, 561 | } 562 | } 563 | 564 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 565 | func (msg *MatchmakerRemoveMsg) BuildEnvelope() *Envelope { 566 | return &Envelope{ 567 | Message: &Envelope_MatchmakerRemove{ 568 | MatchmakerRemove: msg, 569 | }, 570 | } 571 | } 572 | 573 | // Send sends the message to the connection. 574 | func (msg *MatchmakerRemoveMsg) Send(ctx context.Context, conn *Conn) error { 575 | return conn.Send(ctx, msg, empty()) 576 | } 577 | 578 | // Async sends the message to the connection. 579 | func (msg *MatchmakerRemoveMsg) Async(ctx context.Context, conn *Conn, f func(error)) { 580 | go func() { 581 | if err := msg.Send(ctx, conn); f != nil { 582 | f(err) 583 | } 584 | }() 585 | } 586 | 587 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 588 | func (msg *MatchmakerTicketMsg) BuildEnvelope() *Envelope { 589 | return &Envelope{ 590 | Message: &Envelope_MatchmakerTicket{ 591 | MatchmakerTicket: msg, 592 | }, 593 | } 594 | } 595 | 596 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 597 | func (msg *NotificationsMsg) BuildEnvelope() *Envelope { 598 | return &Envelope{ 599 | Message: &Envelope_Notifications{ 600 | Notifications: msg, 601 | }, 602 | } 603 | } 604 | 605 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 606 | func (msg *PartyMsg) BuildEnvelope() *Envelope { 607 | return &Envelope{ 608 | Message: &Envelope_Party{ 609 | Party: msg, 610 | }, 611 | } 612 | } 613 | 614 | // PartyAccept creates a realtime message to accept a party member. 615 | func PartyAccept(partyId string, presence *UserPresenceMsg) *PartyAcceptMsg { 616 | return &PartyAcceptMsg{ 617 | PartyId: partyId, 618 | Presence: presence, 619 | } 620 | } 621 | 622 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 623 | func (msg *PartyAcceptMsg) BuildEnvelope() *Envelope { 624 | return &Envelope{ 625 | Message: &Envelope_PartyAccept{ 626 | PartyAccept: msg, 627 | }, 628 | } 629 | } 630 | 631 | // Send sends the message to the connection. 632 | func (msg *PartyAcceptMsg) Send(ctx context.Context, conn *Conn) error { 633 | return conn.Send(ctx, msg, empty()) 634 | } 635 | 636 | // Async sends the message to the connection. 637 | func (msg *PartyAcceptMsg) Async(ctx context.Context, conn *Conn, f func(error)) { 638 | go func() { 639 | if err := msg.Send(ctx, conn); f != nil { 640 | f(err) 641 | } 642 | }() 643 | } 644 | 645 | // PartyClose creates a realtime message to close a party, kicking all party members. 646 | func PartyClose(partyId string) *PartyCloseMsg { 647 | return &PartyCloseMsg{ 648 | PartyId: partyId, 649 | } 650 | } 651 | 652 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 653 | func (msg *PartyCloseMsg) BuildEnvelope() *Envelope { 654 | return &Envelope{ 655 | Message: &Envelope_PartyClose{ 656 | PartyClose: msg, 657 | }, 658 | } 659 | } 660 | 661 | // Send sends the message to the connection. 662 | func (msg *PartyCloseMsg) Send(ctx context.Context, conn *Conn) error { 663 | return conn.Send(ctx, msg, empty()) 664 | } 665 | 666 | // Async sends the message to the connection. 667 | func (msg *PartyCloseMsg) Async(ctx context.Context, conn *Conn, f func(error)) { 668 | go func() { 669 | if err := msg.Send(ctx, conn); f != nil { 670 | f(err) 671 | } 672 | }() 673 | } 674 | 675 | // PartyCreate creates a realtime message to create a party. 676 | func PartyCreate(open bool, maxSize int) *PartyCreateMsg { 677 | return &PartyCreateMsg{ 678 | Open: open, 679 | MaxSize: int32(maxSize), 680 | } 681 | } 682 | 683 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 684 | func (msg *PartyCreateMsg) BuildEnvelope() *Envelope { 685 | return &Envelope{ 686 | Message: &Envelope_PartyCreate{ 687 | PartyCreate: msg, 688 | }, 689 | } 690 | } 691 | 692 | // Send sends the message to the connection. 693 | func (msg *PartyCreateMsg) Send(ctx context.Context, conn *Conn) (*PartyMsg, error) { 694 | res := new(PartyMsg) 695 | if err := conn.Send(ctx, msg, res); err != nil { 696 | return nil, err 697 | } 698 | return res, nil 699 | } 700 | 701 | // Async sends the message to the connection. 702 | func (msg *PartyCreateMsg) Async(ctx context.Context, conn *Conn, f func(*PartyMsg, error)) { 703 | go func() { 704 | if res, err := msg.Send(ctx, conn); f != nil { 705 | f(res, err) 706 | } 707 | }() 708 | } 709 | 710 | // PartyDataSend creates a realtime message to send data to a party. 711 | func PartyDataSend(partyId string, opCode OpType, data []byte) *PartyDataSendMsg { 712 | return &PartyDataSendMsg{ 713 | PartyId: partyId, 714 | OpCode: int64(opCode), 715 | Data: data, 716 | } 717 | } 718 | 719 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 720 | func (msg *PartyDataSendMsg) BuildEnvelope() *Envelope { 721 | return &Envelope{ 722 | Message: &Envelope_PartyDataSend{ 723 | PartyDataSend: msg, 724 | }, 725 | } 726 | } 727 | 728 | // Send sends the message to the connection. 729 | func (msg *PartyDataSendMsg) Send(ctx context.Context, conn *Conn) error { 730 | return conn.Send(ctx, msg, empty()) 731 | } 732 | 733 | // Async sends the message to the connection. 734 | func (msg *PartyDataSendMsg) Async(ctx context.Context, conn *Conn, f func(error)) { 735 | go func() { 736 | if err := msg.Send(ctx, conn); f != nil { 737 | f(err) 738 | } 739 | }() 740 | } 741 | 742 | // PartyJoin creates a realtime message to join a party. 743 | func PartyJoin(partyId string) *PartyJoinMsg { 744 | return &PartyJoinMsg{ 745 | PartyId: partyId, 746 | } 747 | } 748 | 749 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 750 | func (msg *PartyJoinMsg) BuildEnvelope() *Envelope { 751 | return &Envelope{ 752 | Message: &Envelope_PartyJoin{ 753 | PartyJoin: msg, 754 | }, 755 | } 756 | } 757 | 758 | // Send sends the message to the connection. 759 | func (msg *PartyJoinMsg) Send(ctx context.Context, conn *Conn) error { 760 | return conn.Send(ctx, msg, empty()) 761 | } 762 | 763 | // Async sends the message to the connection. 764 | func (msg *PartyJoinMsg) Async(ctx context.Context, conn *Conn, f func(error)) { 765 | go func() { 766 | if err := msg.Send(ctx, conn); f != nil { 767 | f(err) 768 | } 769 | }() 770 | } 771 | 772 | // PartyJoinRequests creates a realtime message to request the list of pending join requests for a party. 773 | func PartyJoinRequests(partyId string) *PartyJoinRequestsMsg { 774 | return &PartyJoinRequestsMsg{ 775 | PartyId: partyId, 776 | } 777 | } 778 | 779 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 780 | func (msg *PartyJoinRequestsMsg) BuildEnvelope() *Envelope { 781 | return &Envelope{ 782 | Message: &Envelope_PartyJoinRequestList{ 783 | PartyJoinRequestList: msg, 784 | }, 785 | } 786 | } 787 | 788 | // Send sends the message to the connection. 789 | func (msg *PartyJoinRequestsMsg) Send(ctx context.Context, conn *Conn) (*PartyJoinRequestMsg, error) { 790 | res := new(PartyJoinRequestMsg) 791 | if err := conn.Send(ctx, msg, res); err != nil { 792 | return nil, err 793 | } 794 | return res, nil 795 | } 796 | 797 | // Async sends the message to the connection. 798 | func (msg *PartyJoinRequestsMsg) Async(ctx context.Context, conn *Conn, f func(*PartyJoinRequestMsg, error)) { 799 | go func() { 800 | if res, err := msg.Send(ctx, conn); f != nil { 801 | f(res, err) 802 | } 803 | }() 804 | } 805 | 806 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 807 | func (msg *PartyJoinRequestMsg) BuildEnvelope() *Envelope { 808 | return &Envelope{ 809 | Message: &Envelope_PartyJoinRequest{ 810 | PartyJoinRequest: msg, 811 | }, 812 | } 813 | } 814 | 815 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 816 | func (msg *PartyLeaderMsg) BuildEnvelope() *Envelope { 817 | return &Envelope{ 818 | Message: &Envelope_PartyLeader{ 819 | PartyLeader: msg, 820 | }, 821 | } 822 | } 823 | 824 | // PartyLeave creates a realtime message to leave a party. 825 | func PartyLeave(partyId string) *PartyLeaveMsg { 826 | return &PartyLeaveMsg{ 827 | PartyId: partyId, 828 | } 829 | } 830 | 831 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 832 | func (msg *PartyLeaveMsg) BuildEnvelope() *Envelope { 833 | return &Envelope{ 834 | Message: &Envelope_PartyLeave{ 835 | PartyLeave: msg, 836 | }, 837 | } 838 | } 839 | 840 | // Send sends the message to the connection. 841 | func (msg *PartyLeaveMsg) Send(ctx context.Context, conn *Conn) error { 842 | return conn.Send(ctx, msg, empty()) 843 | } 844 | 845 | // Async sends the message to the connection. 846 | func (msg *PartyLeaveMsg) Async(ctx context.Context, conn *Conn, f func(error)) { 847 | go func() { 848 | if err := msg.Send(ctx, conn); f != nil { 849 | f(err) 850 | } 851 | }() 852 | } 853 | 854 | // PartyMatchmakerAdd creates a realtime message to begin matchmaking as a party. 855 | func PartyMatchmakerAdd(partyId, query string, minCount, maxCount int) *PartyMatchmakerAddMsg { 856 | return &PartyMatchmakerAddMsg{ 857 | PartyId: partyId, 858 | Query: query, 859 | MinCount: int32(minCount), 860 | MaxCount: int32(maxCount), 861 | } 862 | } 863 | 864 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 865 | func (msg *PartyMatchmakerAddMsg) BuildEnvelope() *Envelope { 866 | return &Envelope{ 867 | Message: &Envelope_PartyMatchmakerAdd{ 868 | PartyMatchmakerAdd: msg, 869 | }, 870 | } 871 | } 872 | 873 | // WithStringProperties sets the stringProperties on the message. 874 | func (msg *PartyMatchmakerAddMsg) WithStringProperties(stringProperties map[string]string) *PartyMatchmakerAddMsg { 875 | msg.StringProperties = stringProperties 876 | return msg 877 | } 878 | 879 | // WithNumericProperties sets the stringProperties on the message. 880 | func (msg *PartyMatchmakerAddMsg) WithNumericProperties(numericProperties map[string]float64) *PartyMatchmakerAddMsg { 881 | msg.NumericProperties = numericProperties 882 | return msg 883 | } 884 | 885 | // WithCountMultiple sets the stringProperties on the message. 886 | func (msg *PartyMatchmakerAddMsg) WithCountMultiple(countMultiple int) *PartyMatchmakerAddMsg { 887 | msg.CountMultiple = wrapperspb.Int32(int32(countMultiple)) 888 | return msg 889 | } 890 | 891 | // Send sends the message to the connection. 892 | func (msg *PartyMatchmakerAddMsg) Send(ctx context.Context, conn *Conn) (*PartyMatchmakerTicketMsg, error) { 893 | res := new(PartyMatchmakerTicketMsg) 894 | if err := conn.Send(ctx, msg, res); err != nil { 895 | return nil, err 896 | } 897 | return res, nil 898 | } 899 | 900 | // Async sends the message to the connection. 901 | func (msg *PartyMatchmakerAddMsg) Async(ctx context.Context, conn *Conn, f func(*PartyMatchmakerTicketMsg, error)) { 902 | go func() { 903 | if res, err := msg.Send(ctx, conn); f != nil { 904 | f(res, err) 905 | } 906 | }() 907 | } 908 | 909 | // PartyMatchmakerRemove creates a realtime message to cancel a party matchmaking process for a ticket. 910 | func PartyMatchmakerRemove(partyId, ticket string) *PartyMatchmakerRemoveMsg { 911 | return &PartyMatchmakerRemoveMsg{ 912 | PartyId: partyId, 913 | Ticket: ticket, 914 | } 915 | } 916 | 917 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 918 | func (msg *PartyMatchmakerRemoveMsg) BuildEnvelope() *Envelope { 919 | return &Envelope{ 920 | Message: &Envelope_PartyMatchmakerRemove{ 921 | PartyMatchmakerRemove: msg, 922 | }, 923 | } 924 | } 925 | 926 | // Send sends the message to the connection. 927 | func (msg *PartyMatchmakerRemoveMsg) Send(ctx context.Context, conn *Conn) error { 928 | return conn.Send(ctx, msg, empty()) 929 | } 930 | 931 | // Async sends the message to the connection. 932 | func (msg *PartyMatchmakerRemoveMsg) Async(ctx context.Context, conn *Conn, f func(error)) { 933 | go func() { 934 | if err := msg.Send(ctx, conn); f != nil { 935 | f(err) 936 | } 937 | }() 938 | } 939 | 940 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 941 | func (msg *PartyMatchmakerTicketMsg) BuildEnvelope() *Envelope { 942 | return &Envelope{ 943 | Message: &Envelope_PartyMatchmakerTicket{ 944 | PartyMatchmakerTicket: msg, 945 | }, 946 | } 947 | } 948 | 949 | // PartyPromote creates a realtime message to promote a new party leader. 950 | func PartyPromote(partyId string, presence *UserPresenceMsg) *PartyPromoteMsg { 951 | return &PartyPromoteMsg{ 952 | PartyId: partyId, 953 | Presence: presence, 954 | } 955 | } 956 | 957 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 958 | func (msg *PartyPromoteMsg) BuildEnvelope() *Envelope { 959 | return &Envelope{ 960 | Message: &Envelope_PartyPromote{ 961 | PartyPromote: msg, 962 | }, 963 | } 964 | } 965 | 966 | // Send sends the message to the connection. 967 | func (msg *PartyPromoteMsg) Send(ctx context.Context, conn *Conn) (*PartyLeaderMsg, error) { 968 | res := new(PartyLeaderMsg) 969 | if err := conn.Send(ctx, msg, res); err != nil { 970 | return nil, err 971 | } 972 | return res, nil 973 | } 974 | 975 | // Async sends the message to the connection. 976 | func (msg *PartyPromoteMsg) Async(ctx context.Context, conn *Conn, f func(*PartyLeaderMsg, error)) { 977 | go func() { 978 | if res, err := msg.Send(ctx, conn); f != nil { 979 | f(res, err) 980 | } 981 | }() 982 | } 983 | 984 | // PartyRemove creates a realtime message to kick a party member or decline a request to join. 985 | func PartyRemove(partyId string, presence *UserPresenceMsg) *PartyRemoveMsg { 986 | return &PartyRemoveMsg{ 987 | PartyId: partyId, 988 | Presence: presence, 989 | } 990 | } 991 | 992 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 993 | func (msg *PartyRemoveMsg) BuildEnvelope() *Envelope { 994 | return &Envelope{ 995 | Message: &Envelope_PartyRemove{ 996 | PartyRemove: msg, 997 | }, 998 | } 999 | } 1000 | 1001 | // Send sends the message to the connection. 1002 | func (msg *PartyRemoveMsg) Send(ctx context.Context, conn *Conn) error { 1003 | return conn.Send(ctx, msg, empty()) 1004 | } 1005 | 1006 | // Async sends the message to the connection. 1007 | func (msg *PartyRemoveMsg) Async(ctx context.Context, conn *Conn, f func(error)) { 1008 | go func() { 1009 | if err := msg.Send(ctx, conn); f != nil { 1010 | f(err) 1011 | } 1012 | }() 1013 | } 1014 | 1015 | // Ping creates a realtime message to do a ping. 1016 | func Ping() *PingMsg { 1017 | return &PingMsg{} 1018 | } 1019 | 1020 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 1021 | func (msg *PingMsg) BuildEnvelope() *Envelope { 1022 | return &Envelope{ 1023 | Message: &Envelope_Ping{ 1024 | Ping: msg, 1025 | }, 1026 | } 1027 | } 1028 | 1029 | // Send sends the message to the connection. 1030 | func (msg *PingMsg) Send(ctx context.Context, conn *Conn) error { 1031 | return conn.Send(ctx, msg, empty()) 1032 | } 1033 | 1034 | // Async sends the message to the connection. 1035 | func (msg *PingMsg) Async(ctx context.Context, conn *Conn, f func(error)) { 1036 | go func() { 1037 | if err := msg.Send(ctx, conn); f != nil { 1038 | f(err) 1039 | } 1040 | }() 1041 | } 1042 | 1043 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 1044 | func (msg *RpcMsg) BuildEnvelope() *Envelope { 1045 | return &Envelope{ 1046 | Message: &Envelope_Rpc{ 1047 | Rpc: msg, 1048 | }, 1049 | } 1050 | } 1051 | 1052 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 1053 | func (msg *StatusMsg) BuildEnvelope() *Envelope { 1054 | return &Envelope{ 1055 | Message: &Envelope_Status{ 1056 | Status: msg, 1057 | }, 1058 | } 1059 | } 1060 | 1061 | // StatusFollow creates a realtime message to subscribe to user status updates. 1062 | func StatusFollow(userIds ...string) *StatusFollowMsg { 1063 | return &StatusFollowMsg{ 1064 | UserIds: userIds, 1065 | } 1066 | } 1067 | 1068 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 1069 | func (msg *StatusFollowMsg) BuildEnvelope() *Envelope { 1070 | return &Envelope{ 1071 | Message: &Envelope_StatusFollow{ 1072 | StatusFollow: msg, 1073 | }, 1074 | } 1075 | } 1076 | 1077 | // WithUsernames sets the usernames on the message. 1078 | func (msg *StatusFollowMsg) WithUsernames(usernames ...string) *StatusFollowMsg { 1079 | msg.Usernames = usernames 1080 | return msg 1081 | } 1082 | 1083 | // Send sends the message to the connection. 1084 | func (msg *StatusFollowMsg) Send(ctx context.Context, conn *Conn) (*StatusMsg, error) { 1085 | res := new(StatusMsg) 1086 | if err := conn.Send(ctx, msg, res); err != nil { 1087 | return nil, err 1088 | } 1089 | return res, nil 1090 | } 1091 | 1092 | // Async sends the message to the connection. 1093 | func (msg *StatusFollowMsg) Async(ctx context.Context, conn *Conn, f func(*StatusMsg, error)) { 1094 | go func() { 1095 | if res, err := msg.Send(ctx, conn); f != nil { 1096 | f(res, err) 1097 | } 1098 | }() 1099 | } 1100 | 1101 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 1102 | func (msg *StatusPresenceEventMsg) BuildEnvelope() *Envelope { 1103 | return &Envelope{ 1104 | Message: &Envelope_StatusPresenceEvent{ 1105 | StatusPresenceEvent: msg, 1106 | }, 1107 | } 1108 | } 1109 | 1110 | // StatusUnfollow creates a realtime message to unfollow user's status updates. 1111 | func StatusUnfollow(userIds ...string) *StatusUnfollowMsg { 1112 | return &StatusUnfollowMsg{ 1113 | UserIds: userIds, 1114 | } 1115 | } 1116 | 1117 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 1118 | func (msg *StatusUnfollowMsg) BuildEnvelope() *Envelope { 1119 | return &Envelope{ 1120 | Message: &Envelope_StatusUnfollow{ 1121 | StatusUnfollow: msg, 1122 | }, 1123 | } 1124 | } 1125 | 1126 | // Send sends the message to the connection. 1127 | func (msg *StatusUnfollowMsg) Send(ctx context.Context, conn *Conn) error { 1128 | return conn.Send(ctx, msg, empty()) 1129 | } 1130 | 1131 | // Async sends the message to the connection. 1132 | func (msg *StatusUnfollowMsg) Async(ctx context.Context, conn *Conn, f func(error)) { 1133 | go func() { 1134 | if err := msg.Send(ctx, conn); f != nil { 1135 | f(err) 1136 | } 1137 | }() 1138 | } 1139 | 1140 | // StatusUpdate creates a realtime message to update the user's status. 1141 | func StatusUpdate() *StatusUpdateMsg { 1142 | return &StatusUpdateMsg{} 1143 | } 1144 | 1145 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 1146 | func (msg *StatusUpdateMsg) BuildEnvelope() *Envelope { 1147 | return &Envelope{ 1148 | Message: &Envelope_StatusUpdate{ 1149 | StatusUpdate: msg, 1150 | }, 1151 | } 1152 | } 1153 | 1154 | // WithStatus sets the status on the message. 1155 | func (msg *StatusUpdateMsg) WithStatus(status string) *StatusUpdateMsg { 1156 | msg.Status = wrapperspb.String(status) 1157 | return msg 1158 | } 1159 | 1160 | // Send sends the message to the connection. 1161 | func (msg *StatusUpdateMsg) Send(ctx context.Context, conn *Conn) error { 1162 | return conn.Send(ctx, msg, empty()) 1163 | } 1164 | 1165 | // Async sends the message to the connection. 1166 | func (msg *StatusUpdateMsg) Async(ctx context.Context, conn *Conn, f func(error)) { 1167 | go func() { 1168 | if err := msg.Send(ctx, conn); f != nil { 1169 | f(err) 1170 | } 1171 | }() 1172 | } 1173 | 1174 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 1175 | func (msg *StreamDataMsg) BuildEnvelope() *Envelope { 1176 | return &Envelope{ 1177 | Message: &Envelope_StreamData{ 1178 | StreamData: msg, 1179 | }, 1180 | } 1181 | } 1182 | 1183 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 1184 | func (msg *StreamPresenceEventMsg) BuildEnvelope() *Envelope { 1185 | return &Envelope{ 1186 | Message: &Envelope_StreamPresenceEvent{ 1187 | StreamPresenceEvent: msg, 1188 | }, 1189 | } 1190 | } 1191 | 1192 | // UserPresence creates a new realtime user presence message. 1193 | func UserPresence() *UserPresenceMsg { 1194 | return &UserPresenceMsg{} 1195 | } 1196 | 1197 | // WithUserId sets the user id on the message. 1198 | func (msg *UserPresenceMsg) WithUserId(userId string) *UserPresenceMsg { 1199 | msg.UserId = userId 1200 | return msg 1201 | } 1202 | 1203 | // WithSessionId sets the session id on the message. 1204 | func (msg *UserPresenceMsg) WithSessionId(sessionId string) *UserPresenceMsg { 1205 | msg.SessionId = sessionId 1206 | return msg 1207 | } 1208 | 1209 | // WithUsername sets the username on the message. 1210 | func (msg *UserPresenceMsg) WithUsername(username string) *UserPresenceMsg { 1211 | msg.Username = username 1212 | return msg 1213 | } 1214 | 1215 | // WithPersistence sets the persistence on the message. 1216 | func (msg *UserPresenceMsg) WithPersistence(persistence bool) *UserPresenceMsg { 1217 | msg.Persistence = persistence 1218 | return msg 1219 | } 1220 | 1221 | // WithStatus sets the status on the message. 1222 | func (msg *UserPresenceMsg) WithStatus(status string) *UserPresenceMsg { 1223 | msg.Status = wrapperspb.String(status) 1224 | return msg 1225 | } 1226 | 1227 | // emptyMsg is an empty message. 1228 | type emptyMsg struct{} 1229 | 1230 | // empty creates a new empty message. 1231 | func empty() emptyMsg { 1232 | return emptyMsg{} 1233 | } 1234 | 1235 | // BuildEnvelope satisfies the EnvelopeBuilder interface. 1236 | func (emptyMsg) BuildEnvelope() *Envelope { 1237 | return new(Envelope) 1238 | } 1239 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | package nakama 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "math/rand" 9 | "net/http" 10 | "net/url" 11 | "strconv" 12 | "strings" 13 | "sync" 14 | "sync/atomic" 15 | "time" 16 | 17 | "google.golang.org/protobuf/encoding/protojson" 18 | "google.golang.org/protobuf/proto" 19 | "nhooyr.io/websocket" 20 | ) 21 | 22 | // ConnClientHandler is the interface for connection handlers. 23 | type ConnClientHandler interface { 24 | HttpClient() *http.Client 25 | SocketURL() (string, error) 26 | Token(context.Context) (string, error) 27 | SessionEnd() 28 | Logf(string, ...interface{}) 29 | Errf(string, ...interface{}) 30 | } 31 | 32 | // ConnHandler is an empty interface that provides a clean, extensible way to 33 | // register a type as a handler that is future-proofed. As used by WithConnHandler, 34 | // a type that supports the any of the following smuggled interfaces: 35 | // 36 | // ConnectHandler(context.Context) 37 | // DisconnectHandler(context.Context, error) 38 | // ErrorHandler(context.Context, *nakama.ErrorMsg) 39 | // ChannelMessageHandler(context.Context, *nakama.ChannelMessageMsg) 40 | // ChannelPresenceEventHandler(context.Context, *nakama.ChannelPresenceEventMsg) 41 | // MatchDataHandler(context.Context, *nakama.MatchDataMsg) 42 | // MatchPresenceEventHandler(context.Context, *nakama.MatchPresenceEventMsg) 43 | // MatchmakerMatchedHandler(context.Context, *nakama.MatchmakerMatchedMsg) 44 | // NotificationsHandler(context.Context, *nakama.NotificationsMsg) 45 | // StatusPresenceEventHandler(context.Context, *nakama.StatusPresenceEventMsg) 46 | // StreamDataHandler(context.Context, *nakama.StreamDataMsg) 47 | // StreamPresenceEventHandler(context.Context, *nakama.StreamPresenceEventMsg) 48 | // 49 | // Will have its method added to Conn as its respective Handler. 50 | // 51 | // For an overview of Go interface smuggling as a concept, see: 52 | // 53 | // https://utcc.utoronto.ca/~cks/space/blog/programming/GoInterfaceSmuggling 54 | type ConnHandler interface{} 55 | 56 | // Conn is a nakama realtime websocket connection. 57 | type Conn struct { 58 | h ConnClientHandler 59 | url string 60 | token string 61 | binary bool 62 | query url.Values 63 | persist bool 64 | backoffMin time.Duration 65 | backoffMax time.Duration 66 | backoffFactor float64 67 | backoffRand *rand.Rand 68 | 69 | ctx context.Context 70 | ws *websocket.Conn 71 | cancel func() 72 | stop bool 73 | 74 | id uint64 75 | out chan *res 76 | m map[string]*res 77 | 78 | ConnectHandler func(context.Context) 79 | DisconnectHandler func(context.Context, error) 80 | ErrorHandler func(context.Context, *ErrorMsg) 81 | ChannelMessageHandler func(context.Context, *ChannelMessageMsg) 82 | ChannelPresenceEventHandler func(context.Context, *ChannelPresenceEventMsg) 83 | MatchDataHandler func(context.Context, *MatchDataMsg) 84 | MatchPresenceEventHandler func(context.Context, *MatchPresenceEventMsg) 85 | MatchmakerMatchedHandler func(context.Context, *MatchmakerMatchedMsg) 86 | NotificationsHandler func(context.Context, *NotificationsMsg) 87 | StatusPresenceEventHandler func(context.Context, *StatusPresenceEventMsg) 88 | StreamDataHandler func(context.Context, *StreamDataMsg) 89 | StreamPresenceEventHandler func(context.Context, *StreamPresenceEventMsg) 90 | 91 | rw sync.RWMutex 92 | } 93 | 94 | // NewConn creates a new nakama realtime websocket connection. 95 | func NewConn(ctx context.Context, opts ...ConnOption) (*Conn, error) { 96 | conn := &Conn{ 97 | binary: true, 98 | query: url.Values{}, 99 | backoffMin: 100 * time.Millisecond, 100 | backoffMax: 3 * time.Second, 101 | backoffFactor: 1.2, 102 | backoffRand: rand.New(rand.NewSource(time.Now().UnixNano())), 103 | out: make(chan *res), 104 | m: make(map[string]*res), 105 | stop: true, 106 | } 107 | for _, o := range opts { 108 | o(conn) 109 | } 110 | if err := conn.Open(ctx); err != nil { 111 | return nil, err 112 | } 113 | return conn, nil 114 | } 115 | 116 | // Open opens and persists (when enabled) the websocket connection to the 117 | // Nakama server. 118 | func (conn *Conn) Open(ctx context.Context) error { 119 | if conn.ws != nil { 120 | return nil 121 | } 122 | conn.stop = false 123 | if !conn.persist { 124 | return conn.open(ctx) 125 | } 126 | go conn.run(ctx) 127 | return nil 128 | } 129 | 130 | // run keeps open the websocket connection to the Nakama server when persist is 131 | // enabled. 132 | func (conn *Conn) run(ctx context.Context) { 133 | d, jitter, connected, last := conn.backoffMin, time.Duration(0), false, false 134 | for !conn.stop { 135 | if connected = conn.ws != nil; !connected { 136 | if err := conn.open(ctx); err != nil { 137 | conn.h.Logf("unable to open websocket: %v", err) 138 | } 139 | } 140 | select { 141 | case <-ctx.Done(): 142 | return 143 | case <-time.After(d + jitter): 144 | } 145 | d, jitter = conn.backoffDur(d, last != connected) 146 | last = connected 147 | } 148 | } 149 | 150 | // backoffDur calculates backoff duration. 151 | func (conn *Conn) backoffDur(d time.Duration, reset bool) (time.Duration, time.Duration) { 152 | switch { 153 | case reset: 154 | return conn.backoffMin, 0 155 | case conn.backoffMax <= d: 156 | jitter := time.Duration(0) 157 | if conn.backoffRand != nil { 158 | jitter = time.Duration(conn.backoffRand.Intn(int(conn.backoffMax))) 159 | } 160 | return conn.backoffMax, jitter 161 | } 162 | d = time.Duration(float64(d) * conn.backoffFactor) 163 | jitter := time.Duration(0) 164 | if conn.backoffRand != nil { 165 | jitter = time.Duration(conn.backoffRand.Intn(int(d))) 166 | } 167 | return d, jitter 168 | } 169 | 170 | // open opens the websocket connection to the Nakama server. 171 | func (conn *Conn) open(ctx context.Context) error { 172 | ws, err := conn.dial(ctx) 173 | if err != nil { 174 | return err 175 | } 176 | conn.rw.Lock() 177 | defer conn.rw.Unlock() 178 | ctx, cancel := context.WithCancel(ctx) 179 | conn.ctx, conn.ws, conn.cancel = ctx, ws, cancel 180 | if conn.ConnectHandler != nil { 181 | go conn.ConnectHandler(conn.ctx) 182 | } 183 | // incoming 184 | go func() { 185 | for { 186 | _, r, err := ws.Reader(ctx) 187 | if err != nil { 188 | _ = conn.CloseWithStopErr(!conn.persist, false, err) 189 | return 190 | } 191 | buf, err := io.ReadAll(r) 192 | if err != nil { 193 | _ = conn.CloseWithStopErr(!conn.persist, false, err) 194 | return 195 | } 196 | if buf == nil { 197 | _ = conn.CloseWithStopErr(!conn.persist, false, ErrConnReadEmptyMessage) 198 | return 199 | } 200 | if err := conn.recv(ctx, buf); err != nil { 201 | conn.h.Errf("unable to dispatch incoming message: %v", err) 202 | } 203 | } 204 | }() 205 | // outgoing 206 | go func() { 207 | for { 208 | select { 209 | case <-ctx.Done(): 210 | return 211 | case m := <-conn.out: 212 | id, err := conn.send(ctx, ws, m.msg) 213 | if err != nil { 214 | if !errors.Is(err, context.Canceled) { 215 | conn.h.Errf("unable to send message: %v", err) 216 | } 217 | m.err <- fmt.Errorf("unable to send message: %w", err) 218 | close(m.err) 219 | continue 220 | } 221 | if m.v == nil || id == "" { 222 | close(m.err) 223 | continue 224 | } 225 | conn.rw.Lock() 226 | conn.m[id] = m 227 | conn.rw.Unlock() 228 | } 229 | } 230 | }() 231 | return nil 232 | } 233 | 234 | // send marshals the message and writes it to the websocket connection. 235 | func (conn *Conn) send(ctx context.Context, ws *websocket.Conn, msg EnvelopeBuilder) (string, error) { 236 | env := msg.BuildEnvelope() 237 | env.Cid = strconv.FormatUint(atomic.AddUint64(&conn.id, 1), 10) 238 | buf, err := conn.marshal(env) 239 | if err != nil { 240 | return "", err 241 | } 242 | typ := websocket.MessageBinary 243 | if !conn.binary { 244 | typ = websocket.MessageText 245 | } 246 | if err := ws.Write(ctx, typ, buf); err != nil { 247 | _ = conn.CloseWithStopErr(!conn.persist, false, err) 248 | return "", err 249 | } 250 | return env.Cid, nil 251 | } 252 | 253 | // recv unmarshals buf, dispatching the message. 254 | func (conn *Conn) recv(ctx context.Context, buf []byte) error { 255 | env, err := conn.unmarshal(buf) 256 | switch { 257 | case err != nil: 258 | return fmt.Errorf("unable to unmarshal: %w", err) 259 | case env.Cid == "": 260 | return conn.recvNotify(ctx, env) 261 | } 262 | return conn.recvResponse(ctx, env) 263 | } 264 | 265 | // recvNotify dispaches events and received updates. 266 | func (conn *Conn) recvNotify(ctx context.Context, env *Envelope) error { 267 | switch v := env.Message.(type) { 268 | case *Envelope_Error: 269 | if conn.ErrorHandler != nil { 270 | go conn.ErrorHandler(ctx, v.Error) 271 | } 272 | return v.Error 273 | case *Envelope_ChannelMessage: 274 | if conn.ChannelMessageHandler != nil { 275 | go conn.ChannelMessageHandler(ctx, v.ChannelMessage) 276 | } 277 | return nil 278 | case *Envelope_ChannelPresenceEvent: 279 | if conn.ChannelPresenceEventHandler != nil { 280 | go conn.ChannelPresenceEventHandler(ctx, v.ChannelPresenceEvent) 281 | } 282 | return nil 283 | case *Envelope_MatchData: 284 | if conn.MatchDataHandler != nil { 285 | go conn.MatchDataHandler(ctx, v.MatchData) 286 | } 287 | return nil 288 | case *Envelope_MatchPresenceEvent: 289 | if conn.MatchPresenceEventHandler != nil { 290 | go conn.MatchPresenceEventHandler(ctx, v.MatchPresenceEvent) 291 | } 292 | return nil 293 | case *Envelope_MatchmakerMatched: 294 | if conn.MatchmakerMatchedHandler != nil { 295 | go conn.MatchmakerMatchedHandler(ctx, v.MatchmakerMatched) 296 | } 297 | return nil 298 | case *Envelope_Notifications: 299 | if conn.NotificationsHandler != nil { 300 | go conn.NotificationsHandler(ctx, v.Notifications) 301 | } 302 | return nil 303 | case *Envelope_StatusPresenceEvent: 304 | if conn.StatusPresenceEventHandler != nil { 305 | go conn.StatusPresenceEventHandler(ctx, v.StatusPresenceEvent) 306 | } 307 | return nil 308 | case *Envelope_StreamData: 309 | if conn.StreamDataHandler != nil { 310 | go conn.StreamDataHandler(ctx, v.StreamData) 311 | } 312 | return nil 313 | case *Envelope_StreamPresenceEvent: 314 | if conn.StreamPresenceEventHandler != nil { 315 | go conn.StreamPresenceEventHandler(ctx, v.StreamPresenceEvent) 316 | } 317 | return nil 318 | } 319 | return fmt.Errorf("unknown type %T", env.Message) 320 | } 321 | 322 | // recvResponse dispatches a received response (messages with cid != ""). 323 | func (conn *Conn) recvResponse(ctx context.Context, env *Envelope) error { 324 | conn.rw.RLock() 325 | m, ok := conn.m[env.Cid] 326 | conn.rw.RUnlock() 327 | if !ok || m == nil { 328 | return fmt.Errorf("no callback id %s (%T)", env.Cid, env.Message) 329 | } 330 | // remove and close 331 | defer func() { 332 | close(m.err) 333 | conn.rw.Lock() 334 | delete(conn.m, env.Cid) 335 | conn.rw.Unlock() 336 | }() 337 | // check error 338 | if err, ok := env.Message.(*Envelope_Error); ok { 339 | conn.h.Errf("realtime error: %v", err.Error) 340 | m.err <- err.Error 341 | return nil 342 | } 343 | // ignore response for RPC 344 | if m.v == nil { 345 | return nil 346 | } 347 | // merge 348 | proto.Merge(m.v.BuildEnvelope(), env) 349 | return nil 350 | } 351 | 352 | // Send sends a message. 353 | func (conn *Conn) Send(ctx context.Context, msg, v EnvelopeBuilder) error { 354 | m := &res{ 355 | msg: msg, 356 | v: v, 357 | err: make(chan error, 1), 358 | } 359 | select { 360 | case <-ctx.Done(): 361 | return ctx.Err() 362 | case conn.out <- m: 363 | } 364 | var err error 365 | select { 366 | case <-ctx.Done(): 367 | return ctx.Err() 368 | case err = <-m.err: 369 | } 370 | return err 371 | } 372 | 373 | // Connected returns true when the websocket connection is connected to the 374 | // Nakama server. 375 | func (conn *Conn) Connected() bool { 376 | ws := conn.ws 377 | return ws != nil 378 | } 379 | 380 | // CloseWithStopErr closes the websocket connection with an error. 381 | func (conn *Conn) CloseWithStopErr(stop, force bool, err error) error { 382 | conn.rw.Lock() 383 | defer conn.rw.Unlock() 384 | if conn.ws != nil { 385 | defer conn.ws.Close(websocket.StatusNormalClosure, "closing") 386 | if force { 387 | defer conn.ws.Write(conn.ctx, websocket.MessageText, []byte{'{'}) 388 | } 389 | defer conn.cancel() 390 | for k := range conn.m { 391 | delete(conn.m, k) 392 | } 393 | if conn.DisconnectHandler != nil { 394 | go conn.DisconnectHandler(conn.ctx, err) 395 | } 396 | conn.stop, conn.ctx, conn.ws, conn.cancel = stop, nil, nil, nil 397 | } 398 | return nil 399 | } 400 | 401 | // CloseWithErr closes the websocket connection with an error. 402 | func (conn *Conn) CloseWithErr(err error) error { 403 | return conn.CloseWithStopErr(true, true, err) 404 | } 405 | 406 | // Close closes the websocket connection. 407 | func (conn *Conn) Close() error { 408 | return conn.CloseWithStopErr(true, true, nil) 409 | } 410 | 411 | // dial creates a new websocket connection to the Nakama server. 412 | func (conn *Conn) dial(ctx context.Context) (*websocket.Conn, error) { 413 | urlstr, opts, err := conn.dialParams(ctx) 414 | if err != nil { 415 | return nil, fmt.Errorf("unable to create dial params: %w", err) 416 | } 417 | conn.h.Logf("connecting %s", urlstr) 418 | ws, _, err := websocket.Dial(ctx, urlstr, opts) 419 | if err != nil { 420 | conn.h.SessionEnd() 421 | return nil, fmt.Errorf("unable to connect to %s: %w", urlstr, err) 422 | } 423 | return ws, nil 424 | } 425 | 426 | // dialParams builds the dial parameters for the nakama server. 427 | func (conn *Conn) dialParams(ctx context.Context) (string, *websocket.DialOptions, error) { 428 | // build url 429 | urlstr := conn.url 430 | if urlstr == "" && conn.h != nil { 431 | var err error 432 | if urlstr, err = conn.h.SocketURL(); err != nil { 433 | return "", nil, err 434 | } 435 | } 436 | // build token 437 | token := conn.token 438 | if token == "" && conn.h != nil { 439 | var err error 440 | if token, err = conn.h.Token(ctx); err != nil { 441 | return "", nil, err 442 | } 443 | } 444 | // build query 445 | query := url.Values{} 446 | for k, v := range conn.query { 447 | query[k] = v 448 | } 449 | query.Set("token", token) 450 | format := "protobuf" 451 | if !conn.binary { 452 | format = "json" 453 | } 454 | query.Set("format", format) 455 | httpClient := http.DefaultClient 456 | if conn.h != nil { 457 | httpClient = conn.h.HttpClient() 458 | } 459 | return urlstr + "?" + query.Encode(), buildWsOptions(httpClient), nil 460 | } 461 | 462 | // marshal marshals the message. If the format set on the connection is json, 463 | // then the message will be marshaled using json encoding. 464 | func (conn *Conn) marshal(env *Envelope) ([]byte, error) { 465 | f := proto.Marshal 466 | if !conn.binary { 467 | f = protojson.Marshal 468 | } 469 | return f(env) 470 | } 471 | 472 | // unmarshal unmarshals the message. If the format set on the connection is 473 | // json, then v will be unmarshaled using json encoding. 474 | func (conn *Conn) unmarshal(buf []byte) (*Envelope, error) { 475 | f := proto.Unmarshal 476 | if !conn.binary { 477 | f = protojson.Unmarshal 478 | } 479 | env := new(Envelope) 480 | if err := f(buf, env); err != nil { 481 | return nil, err 482 | } 483 | return env, nil 484 | } 485 | 486 | // ChannelJoin sends a message to join a chat channel. 487 | func (conn *Conn) ChannelJoin(ctx context.Context, target string, typ ChannelType, persistence, hidden bool) (*ChannelMsg, error) { 488 | return ChannelJoin(target, typ). 489 | WithPersistence(persistence). 490 | WithHidden(hidden). 491 | Send(ctx, conn) 492 | } 493 | 494 | // ChannelJoinAsync sends a message to join a chat channel. 495 | func (conn *Conn) ChannelJoinAsync(ctx context.Context, target string, typ ChannelType, persistence, hidden bool, f func(*ChannelMsg, error)) { 496 | ChannelJoin(target, typ). 497 | WithPersistence(persistence). 498 | WithHidden(hidden). 499 | Async(ctx, conn, f) 500 | } 501 | 502 | // ChannelLeave sends a message to leave a chat channel. 503 | func (conn *Conn) ChannelLeave(ctx context.Context, channelId string) error { 504 | return ChannelLeave(channelId).Send(ctx, conn) 505 | } 506 | 507 | // ChannelLeaveAsync sends a message to leave a chat channel. 508 | func (conn *Conn) ChannelLeaveAsync(ctx context.Context, channelId string, f func(error)) { 509 | ChannelLeave(channelId).Async(ctx, conn, f) 510 | } 511 | 512 | // ChannelMessageRemove sends a message to remove a message from a channel. 513 | func (conn *Conn) ChannelMessageRemove(ctx context.Context, channelId, messageId string) (*ChannelMessageAckMsg, error) { 514 | return ChannelMessageRemove(channelId, messageId).Send(ctx, conn) 515 | } 516 | 517 | // ChannelMessageRemoveAsync sends a message to remove a message from a channel. 518 | func (conn *Conn) ChannelMessageRemoveAsync(ctx context.Context, channelId, messageId string, f func(*ChannelMessageAckMsg, error)) { 519 | ChannelMessageRemove(channelId, messageId).Async(ctx, conn, f) 520 | } 521 | 522 | // ChannelMessageSend sends a message on a channel. 523 | func (conn *Conn) ChannelMessageSend(ctx context.Context, channelId string, v interface{}) (*ChannelMessageAckMsg, error) { 524 | msg, err := ChannelMessageSend(channelId, v) 525 | if err != nil { 526 | return nil, err 527 | } 528 | return msg.Send(ctx, conn) 529 | } 530 | 531 | // ChannelMessageSendAsync sends a message on a channel. 532 | func (conn *Conn) ChannelMessageSendAsync(ctx context.Context, channelId string, v interface{}, f func(*ChannelMessageAckMsg, error)) { 533 | if msg, err := ChannelMessageSend(channelId, v); err == nil { 534 | f(nil, err) 535 | } else { 536 | msg.Async(ctx, conn, f) 537 | } 538 | } 539 | 540 | // ChannelMessageSendRaw sends a message on a channel. 541 | func (conn *Conn) ChannelMessageSendRaw(ctx context.Context, channelId, content string) (*ChannelMessageAckMsg, error) { 542 | return ChannelMessageSendRaw(channelId, content).Send(ctx, conn) 543 | } 544 | 545 | // ChannelMessageSendAsync sends a message on a channel. 546 | func (conn *Conn) ChannelMessageSendRawAsync(ctx context.Context, channelId, content string, f func(*ChannelMessageAckMsg, error)) { 547 | ChannelMessageSendRaw(channelId, content).Async(ctx, conn, f) 548 | } 549 | 550 | // ChannelMessageUpdate sends a message to update a message on a channel. 551 | func (conn *Conn) ChannelMessageUpdate(ctx context.Context, channelId, messageId string, v interface{}) (*ChannelMessageAckMsg, error) { 552 | msg, err := ChannelMessageUpdate(channelId, messageId, v) 553 | if err != nil { 554 | return nil, err 555 | } 556 | return msg.Send(ctx, conn) 557 | } 558 | 559 | // ChannelMessageUpdateAsync sends a message to update a message on a channel. 560 | func (conn *Conn) ChannelMessageUpdateAsync(ctx context.Context, channelId, messageId string, v interface{}, f func(*ChannelMessageAckMsg, error)) { 561 | if msg, err := ChannelMessageUpdate(channelId, messageId, v); err == nil { 562 | f(nil, err) 563 | } else { 564 | msg.Async(ctx, conn, f) 565 | } 566 | } 567 | 568 | // ChannelMessageUpdateRaw sends a message to update a message on a channel. 569 | func (conn *Conn) ChannelMessageUpdateRaw(ctx context.Context, channelId, messageId, content string) (*ChannelMessageAckMsg, error) { 570 | return ChannelMessageUpdateRaw(channelId, messageId, content).Send(ctx, conn) 571 | } 572 | 573 | // ChannelMessageUpdateRawAsync sends a message to update a message on a channel. 574 | func (conn *Conn) ChannelMessageUpdateRawAsync(ctx context.Context, channelId, messageId, content string, f func(*ChannelMessageAckMsg, error)) { 575 | ChannelMessageUpdateRaw(channelId, messageId, content).Async(ctx, conn, f) 576 | } 577 | 578 | // MatchCreate sends a message to create a multiplayer match. 579 | func (conn *Conn) MatchCreate(ctx context.Context, name string) (*MatchMsg, error) { 580 | return MatchCreate(name).Send(ctx, conn) 581 | } 582 | 583 | // MatchCreateAsync sends a message to create a multiplayer match. 584 | func (conn *Conn) MatchCreateAsync(ctx context.Context, name string, f func(*MatchMsg, error)) { 585 | MatchCreate(name).Async(ctx, conn, f) 586 | } 587 | 588 | // MatchJoin sends a message to join a match. 589 | func (conn *Conn) MatchJoin(ctx context.Context, matchId string, metadata map[string]string) (*MatchMsg, error) { 590 | return MatchJoin(matchId). 591 | WithMetadata(metadata). 592 | Send(ctx, conn) 593 | } 594 | 595 | // MatchJoinAsync sends a message to join a match. 596 | func (conn *Conn) MatchJoinAsync(ctx context.Context, matchId string, metadata map[string]string, f func(*MatchMsg, error)) { 597 | MatchJoin(matchId). 598 | WithMetadata(metadata). 599 | Async(ctx, conn, f) 600 | } 601 | 602 | // MatchJoinToken sends a message to join a match with a token. 603 | func (conn *Conn) MatchJoinToken(ctx context.Context, token string, metadata map[string]string) (*MatchMsg, error) { 604 | return MatchJoinToken(token). 605 | WithMetadata(metadata). 606 | Send(ctx, conn) 607 | } 608 | 609 | // MatchJoinTokenAsync sends a message to join a match with a token. 610 | func (conn *Conn) MatchJoinTokenAsync(ctx context.Context, token string, metadata map[string]string, f func(*MatchMsg, error)) { 611 | MatchJoinToken(token). 612 | WithMetadata(metadata). 613 | Async(ctx, conn, f) 614 | } 615 | 616 | // MatchLeave sends a message to leave a multiplayer match. 617 | func (conn *Conn) MatchLeave(ctx context.Context, matchId string) error { 618 | return MatchLeave(matchId).Send(ctx, conn) 619 | } 620 | 621 | // MatchLeaveAsync sends a message to leave a multiplayer match. 622 | func (conn *Conn) MatchLeaveAsync(ctx context.Context, matchId string, f func(error)) { 623 | MatchLeave(matchId).Async(ctx, conn, f) 624 | } 625 | 626 | // MatchmakerAdd sends a message to join the matchmaker pool and search for opponents on the server. 627 | func (conn *Conn) MatchmakerAdd(ctx context.Context, msg *MatchmakerAddMsg) (*MatchmakerTicketMsg, error) { 628 | return msg.Send(ctx, conn) 629 | } 630 | 631 | // MatchmakerAddAsync sends a message to join the matchmaker pool and search for opponents on the server. 632 | func (conn *Conn) MatchmakerAddAsync(ctx context.Context, msg *MatchmakerAddMsg, f func(*MatchmakerTicketMsg, error)) { 633 | msg.Async(ctx, conn, f) 634 | } 635 | 636 | // MatchmakerRemove sends a message to leave the matchmaker pool for a ticket. 637 | func (conn *Conn) MatchmakerRemove(ctx context.Context, ticket string) error { 638 | return MatchmakerRemove(ticket).Send(ctx, conn) 639 | } 640 | 641 | // MatchmakerRemoveAsync sends a message to leave the matchmaker pool for a ticket. 642 | func (conn *Conn) MatchmakerRemoveAsync(ctx context.Context, ticket string, f func(error)) { 643 | MatchmakerRemove(ticket).Async(ctx, conn, f) 644 | } 645 | 646 | // MatchDataSend sends a message to send input to a multiplayer match. 647 | func (conn *Conn) MatchDataSend(ctx context.Context, matchId string, opCode int64, data []byte, reliable bool, presences ...*UserPresenceMsg) error { 648 | return MatchDataSend(matchId, opCode, data). 649 | WithPresences(presences...). 650 | WithReliable(reliable). 651 | Send(ctx, conn) 652 | } 653 | 654 | // MatchDataSendAsync sends a message to send input to a multiplayer match. 655 | func (conn *Conn) MatchDataSendAsync(ctx context.Context, matchId string, opCode int64, data []byte, reliable bool, presences []*UserPresenceMsg, f func(error)) { 656 | MatchDataSend(matchId, opCode, data). 657 | WithPresences(presences...). 658 | WithReliable(reliable). 659 | Async(ctx, conn, f) 660 | } 661 | 662 | // PartyAccept sends a message to accept a party member. 663 | func (conn *Conn) PartyAccept(ctx context.Context, partyId string, presence *UserPresenceMsg) error { 664 | return PartyAccept(partyId, presence).Send(ctx, conn) 665 | } 666 | 667 | // PartyAcceptAsync sends a message to accept a party member. 668 | func (conn *Conn) PartyAcceptAsync(ctx context.Context, partyId string, presence *UserPresenceMsg, f func(error)) { 669 | PartyAccept(partyId, presence).Async(ctx, conn, f) 670 | } 671 | 672 | // PartyClose sends a message closes a party, kicking all party members. 673 | func (conn *Conn) PartyClose(ctx context.Context, partyId string) error { 674 | return PartyClose(partyId).Send(ctx, conn) 675 | } 676 | 677 | // PartyCloseAsync sends a message closes a party, kicking all party members. 678 | func (conn *Conn) PartyCloseAsync(ctx context.Context, partyId string, f func(error)) { 679 | PartyClose(partyId).Async(ctx, conn, f) 680 | } 681 | 682 | // PartyCreate sends a message to create a party. 683 | func (conn *Conn) PartyCreate(ctx context.Context, open bool, maxSize int) (*PartyMsg, error) { 684 | return PartyCreate(open, maxSize).Send(ctx, conn) 685 | } 686 | 687 | // PartyCreateAsync sends a message to create a party. 688 | func (conn *Conn) PartyCreateAsync(ctx context.Context, open bool, maxSize int, f func(*PartyMsg, error)) { 689 | PartyCreate(open, maxSize).Async(ctx, conn, f) 690 | } 691 | 692 | // PartyDataSend sends a message to send input to a multiplayer party. 693 | func (conn *Conn) PartyDataSend(ctx context.Context, partyId string, opCode OpType, data []byte, reliable bool, presences ...*UserPresenceMsg) error { 694 | return PartyDataSend(partyId, opCode, data).Send(ctx, conn) 695 | } 696 | 697 | // PartyDataSendAsync sends a message to send input to a multiplayer party. 698 | func (conn *Conn) PartyDataSendAsync(ctx context.Context, partyId string, opCode OpType, data []byte, reliable bool, presences []*UserPresenceMsg, f func(error)) { 699 | PartyDataSend(partyId, opCode, data).Async(ctx, conn, f) 700 | } 701 | 702 | // PartyJoin sends a message to join a party. 703 | func (conn *Conn) PartyJoin(ctx context.Context, partyId string) error { 704 | return PartyJoin(partyId).Send(ctx, conn) 705 | } 706 | 707 | // PartyJoinAsync sends a message to join a party. 708 | func (conn *Conn) PartyJoinAsync(ctx context.Context, partyId string, f func(error)) { 709 | PartyJoin(partyId).Async(ctx, conn, f) 710 | } 711 | 712 | // PartyJoinRequests sends a message to request the list of pending join requests for a party. 713 | func (conn *Conn) PartyJoinRequests(ctx context.Context, partyId string) (*PartyJoinRequestMsg, error) { 714 | return PartyJoinRequests(partyId).Send(ctx, conn) 715 | } 716 | 717 | // PartyJoinRequestsAsync sends a message to request the list of pending join requests for a party. 718 | func (conn *Conn) PartyJoinRequestsAsync(ctx context.Context, partyId string, f func(*PartyJoinRequestMsg, error)) { 719 | PartyJoinRequests(partyId).Async(ctx, conn, f) 720 | } 721 | 722 | // PartyLeave sends a message to leave a party. 723 | func (conn *Conn) PartyLeave(ctx context.Context, partyId string) error { 724 | return PartyLeave(partyId).Send(ctx, conn) 725 | } 726 | 727 | // PartyLeaveAsync sends a message to leave a party. 728 | func (conn *Conn) PartyLeaveAsync(ctx context.Context, partyId string, f func(error)) { 729 | PartyLeave(partyId).Async(ctx, conn, f) 730 | } 731 | 732 | // PartyMatchmakerAdd sends a message to begin matchmaking as a party. 733 | func (conn *Conn) PartyMatchmakerAdd(ctx context.Context, partyId, query string, minCount, maxCount int) (*PartyMatchmakerTicketMsg, error) { 734 | return PartyMatchmakerAdd(partyId, query, minCount, maxCount).Send(ctx, conn) 735 | } 736 | 737 | // PartyMatchmakerAddAsync sends a message to begin matchmaking as a party. 738 | func (conn *Conn) PartyMatchmakerAddAsync(ctx context.Context, partyId, query string, minCount, maxCount int, f func(*PartyMatchmakerTicketMsg, error)) { 739 | PartyMatchmakerAdd(partyId, query, minCount, maxCount).Async(ctx, conn, f) 740 | } 741 | 742 | // PartyMatchmakerRemove sends a message to cancel a party matchmaking process for a ticket. 743 | func (conn *Conn) PartyMatchmakerRemove(ctx context.Context, partyId, ticket string) error { 744 | return PartyMatchmakerRemove(partyId, ticket).Send(ctx, conn) 745 | } 746 | 747 | // PartyMatchmakerRemoveAsync sends a message to cancel a party matchmaking process for a ticket. 748 | func (conn *Conn) PartyMatchmakerRemoveAsync(ctx context.Context, partyId, ticket string, f func(error)) { 749 | PartyMatchmakerRemove(partyId, ticket).Async(ctx, conn, f) 750 | } 751 | 752 | // PartyPromote sends a message to promote a new party leader. 753 | func (conn *Conn) PartyPromote(ctx context.Context, partyId string, presence *UserPresenceMsg) (*PartyLeaderMsg, error) { 754 | return PartyPromote(partyId, presence).Send(ctx, conn) 755 | } 756 | 757 | // PartyPromoteAsync sends a message to promote a new party leader. 758 | func (conn *Conn) PartyPromoteAsync(ctx context.Context, partyId string, presence *UserPresenceMsg, f func(*PartyLeaderMsg, error)) { 759 | PartyPromote(partyId, presence).Async(ctx, conn, f) 760 | } 761 | 762 | // PartyRemove sends a message to kick a party member or decline a request to join. 763 | func (conn *Conn) PartyRemove(ctx context.Context, partyId string, presence *UserPresenceMsg) error { 764 | return PartyRemove(partyId, presence).Send(ctx, conn) 765 | } 766 | 767 | // PartyRemoveAsync sends a message to kick a party member or decline a request to join. 768 | func (conn *Conn) PartyRemoveAsync(ctx context.Context, partyId string, presence *UserPresenceMsg, f func(error)) { 769 | PartyRemove(partyId, presence).Async(ctx, conn, f) 770 | } 771 | 772 | // Ping sends a message to do a ping. 773 | func (conn *Conn) Ping(ctx context.Context) error { 774 | return Ping().Send(ctx, conn) 775 | } 776 | 777 | // PingAsync sends a message to do a ping. 778 | func (conn *Conn) PingAsync(ctx context.Context, f func(error)) { 779 | Ping().Async(ctx, conn, f) 780 | } 781 | 782 | // Rpc sends a message to execute a remote procedure call. 783 | func (conn *Conn) Rpc(ctx context.Context, id string, payload, v interface{}) error { 784 | return Rpc(id, payload, v).Send(ctx, conn) 785 | } 786 | 787 | // RpcAsync sends a message to execute a remote procedure call. 788 | func (conn *Conn) RpcAsync(ctx context.Context, id string, payload, v interface{}, f func(error)) { 789 | Rpc(id, payload, v).SendAsync(ctx, conn, f) 790 | } 791 | 792 | // StatusFollow sends a message to subscribe to user status updates. 793 | func (conn *Conn) StatusFollow(ctx context.Context, userIds ...string) (*StatusMsg, error) { 794 | return StatusFollow(userIds...).Send(ctx, conn) 795 | } 796 | 797 | // StatusFollowAsync sends a message to subscribe to user status updates. 798 | func (conn *Conn) StatusFollowAsync(ctx context.Context, userIds []string, f func(*StatusMsg, error)) { 799 | StatusFollow(userIds...).Async(ctx, conn, f) 800 | } 801 | 802 | // StatusUnfollow sends a message to unfollow user's status updates. 803 | func (conn *Conn) StatusUnfollow(ctx context.Context, userIds ...string) error { 804 | return StatusUnfollow(userIds...).Send(ctx, conn) 805 | } 806 | 807 | // StatusUnfollowAsync sends a message to unfollow user's status updates. 808 | func (conn *Conn) StatusUnfollowAsync(ctx context.Context, userIds []string, f func(error)) { 809 | StatusUnfollow(userIds...).Async(ctx, conn, f) 810 | } 811 | 812 | // StatusUpdate sends a message to update the user's status. 813 | func (conn *Conn) StatusUpdate(ctx context.Context, status string) error { 814 | return StatusUpdate(). 815 | WithStatus(status). 816 | Send(ctx, conn) 817 | } 818 | 819 | // StatusUpdateAsync sends a message to update the user's status. 820 | func (conn *Conn) StatusUpdateAsync(ctx context.Context, status string, f func(error)) { 821 | StatusUpdate(). 822 | WithStatus(status). 823 | Async(ctx, conn, f) 824 | } 825 | 826 | // res wraps a request and results. 827 | type res struct { 828 | msg EnvelopeBuilder 829 | v EnvelopeBuilder 830 | err chan error 831 | } 832 | 833 | // ConnOption is a nakama realtime websocket connection option. 834 | type ConnOption func(*Conn) 835 | 836 | // WithConnClientHandler is a nakama websocket connection option to set the 837 | // ClientHandler used. 838 | func WithConnClientHandler(h ConnClientHandler) ConnOption { 839 | return func(conn *Conn) { 840 | conn.h = h 841 | } 842 | } 843 | 844 | // WithConnUrl is a nakama websocket connection option to set the websocket 845 | // URL. 846 | func WithConnUrl(urlstr string) ConnOption { 847 | return func(conn *Conn) { 848 | conn.url = urlstr 849 | } 850 | } 851 | 852 | // WithConnToken is a nakama websocket connection option to set the auth token 853 | // for the websocket. 854 | func WithConnToken(token string) ConnOption { 855 | return func(conn *Conn) { 856 | conn.token = token 857 | } 858 | } 859 | 860 | // WithConnFormat is a nakama websocket connection option to set the message 861 | // encoding format (either "json" or "protobuf"). 862 | func WithConnFormat(format string) ConnOption { 863 | return func(conn *Conn) { 864 | switch s := strings.ToLower(format); s { 865 | case "protobuf": 866 | conn.binary = true 867 | case "json": 868 | conn.binary = false 869 | default: 870 | panic(fmt.Sprintf("invalid websocket format %q", format)) 871 | } 872 | } 873 | } 874 | 875 | // WithConnQuery is a nakama websocket connection option to add an additional 876 | // key/value query param on the websocket URL. 877 | // 878 | // Note: this should not be used to set "token" or "format". Use WithConnToken 879 | // and WithConnFormat, respectively, to change the token and format query 880 | // params. 881 | func WithConnQuery(key, value string) ConnOption { 882 | return func(conn *Conn) { 883 | conn.query.Set(key, value) 884 | } 885 | } 886 | 887 | // WithConnLang is a nakama websocket connection option to set the lang query 888 | // param on the websocket URL. 889 | func WithConnLang(lang string) ConnOption { 890 | return func(conn *Conn) { 891 | conn.query.Set("lang", lang) 892 | } 893 | } 894 | 895 | // WithConnCreateStatus is a nakama websocket connection option to set the 896 | // status query param on the websocket URL. 897 | func WithConnCreateStatus(status bool) ConnOption { 898 | return func(conn *Conn) { 899 | conn.query.Set("status", strconv.FormatBool(status)) 900 | } 901 | } 902 | 903 | // WithConnPersist is a nakama websocket connection option to enable keeping 904 | // open a persistent connection to the Nakama server. 905 | func WithConnPersist(persist bool) ConnOption { 906 | return func(conn *Conn) { 907 | conn.persist = persist 908 | } 909 | } 910 | 911 | // WithConnBackoff is a nakama websocket connection option to set the 912 | // connection backoff (retry) settings. 913 | func WithConnBackoff(backoffMin, backoffMax time.Duration, backoffFactor float64) ConnOption { 914 | return func(conn *Conn) { 915 | conn.backoffMin, conn.backoffMax, conn.backoffFactor = backoffMin, backoffMax, backoffFactor 916 | } 917 | } 918 | 919 | // WithConnBackoff is a nakama websocket connection option to set the 920 | // backoff jitter random source. 921 | func WithConnBackoffRand(backoffRand *rand.Rand) ConnOption { 922 | return func(conn *Conn) { 923 | conn.backoffRand = backoffRand 924 | } 925 | } 926 | 927 | // WithConnHandler is a nakama websocket connection option to set the 928 | // connection's message handlers. See the ConnHandler type for documentation on 929 | // supported interfaces. 930 | // 931 | // WithConnHandler works by "smuggling" interfaces. That is, WithConnHandler 932 | // checks via a type cast when the underlying type supports methods of the 933 | // following format: 934 | // 935 | // interface{ 936 | // Handler(context.Context, *Msg) 937 | // } 938 | // 939 | // If the ConnHandler's underlying type supports the above, then the 940 | // ConnHandler's Handler method will be set as 941 | // Conn.Handler. For example, given the following: 942 | // 943 | // type MyClient struct{} 944 | // 945 | // func (cl *MyClient) MatchDataHandler(context.Context, *nakama.MatchDataMsg) {} 946 | // func (cl *MyClient) NotificationsHandler(context.Context, *nakama.NotificationsMsg) {} 947 | // 948 | // The following: 949 | // 950 | // cl := nakama.New(/* ... */) 951 | // myClient := &MyClient{} 952 | // conn, err := cl.NewConn(ctx, nakama.WithConnHandler(myClient)) 953 | // 954 | // Is equivalent to: 955 | // 956 | // cl := nakama.New(/* ... */) 957 | // myClient := &MyClient{} 958 | // conn, err := cl.NewConn(ctx) 959 | // conn.MatchDataHandler = myClient.MatchDataHandler 960 | // conn.NotificationsHandler = myClient.NotificationsHandler 961 | // 962 | // For an overview of Go interface smuggling as a concept, see: 963 | // 964 | // https://utcc.utoronto.ca/~cks/space/blog/programming/GoInterfaceSmuggling 965 | func WithConnHandler(handler ConnHandler) ConnOption { 966 | return func(conn *Conn) { 967 | if x, ok := handler.(interface { 968 | ConnectHandler(context.Context) 969 | }); ok { 970 | conn.ConnectHandler = x.ConnectHandler 971 | } 972 | if x, ok := handler.(interface { 973 | DisconnectHandler(context.Context, error) 974 | }); ok { 975 | conn.DisconnectHandler = x.DisconnectHandler 976 | } 977 | if x, ok := handler.(interface { 978 | ErrorHandler(context.Context, *ErrorMsg) 979 | }); ok { 980 | conn.ErrorHandler = x.ErrorHandler 981 | } 982 | if x, ok := handler.(interface { 983 | ChannelMessageHandler(context.Context, *ChannelMessageMsg) 984 | }); ok { 985 | conn.ChannelMessageHandler = x.ChannelMessageHandler 986 | } 987 | if x, ok := handler.(interface { 988 | ChannelPresenceEventHandler(context.Context, *ChannelPresenceEventMsg) 989 | }); ok { 990 | conn.ChannelPresenceEventHandler = x.ChannelPresenceEventHandler 991 | } 992 | if x, ok := handler.(interface { 993 | MatchDataHandler(context.Context, *MatchDataMsg) 994 | }); ok { 995 | conn.MatchDataHandler = x.MatchDataHandler 996 | } 997 | if x, ok := handler.(interface { 998 | MatchPresenceEventHandler(context.Context, *MatchPresenceEventMsg) 999 | }); ok { 1000 | conn.MatchPresenceEventHandler = x.MatchPresenceEventHandler 1001 | } 1002 | if x, ok := handler.(interface { 1003 | MatchmakerMatchedHandler(context.Context, *MatchmakerMatchedMsg) 1004 | }); ok { 1005 | conn.MatchmakerMatchedHandler = x.MatchmakerMatchedHandler 1006 | } 1007 | if x, ok := handler.(interface { 1008 | NotificationsHandler(context.Context, *NotificationsMsg) 1009 | }); ok { 1010 | conn.NotificationsHandler = x.NotificationsHandler 1011 | } 1012 | if x, ok := handler.(interface { 1013 | StatusPresenceEventHandler(context.Context, *StatusPresenceEventMsg) 1014 | }); ok { 1015 | conn.StatusPresenceEventHandler = x.StatusPresenceEventHandler 1016 | } 1017 | if x, ok := handler.(interface { 1018 | StreamDataHandler(context.Context, *StreamDataMsg) 1019 | }); ok { 1020 | conn.StreamDataHandler = x.StreamDataHandler 1021 | } 1022 | if x, ok := handler.(interface { 1023 | StreamPresenceEventHandler(context.Context, *StreamPresenceEventMsg) 1024 | }); ok { 1025 | conn.StreamPresenceEventHandler = x.StreamPresenceEventHandler 1026 | } 1027 | } 1028 | } 1029 | 1030 | // ConnError is a websocket connection error. 1031 | type ConnError string 1032 | 1033 | const ( 1034 | // ErrConnAlreadyOpen is the conn already open error. 1035 | ErrConnAlreadyOpen ConnError = "conn already open" 1036 | // ErrConnReadEmptyMessage is the conn read empty message error. 1037 | ErrConnReadEmptyMessage ConnError = "conn read empty message" 1038 | ) 1039 | 1040 | // Error satisfies the error interface. 1041 | func (err ConnError) Error() string { 1042 | return string(err) 1043 | } 1044 | -------------------------------------------------------------------------------- /nakama.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Nakama Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * The Nakama server RPC protocol for games and apps. 17 | */ 18 | syntax = "proto3"; 19 | 20 | package nakama; 21 | 22 | import "google/protobuf/timestamp.proto"; 23 | import "google/protobuf/wrappers.proto"; 24 | 25 | option go_package = "github.com/ascii8/nakama-go;nakama"; 26 | 27 | option java_multiple_files = true; 28 | option java_outer_classname = "NakamaApi"; 29 | option java_package = "com.heroiclabs.nakama.api"; 30 | 31 | option csharp_namespace = "Nakama.Protobuf"; 32 | 33 | // Healthcheck request. 34 | message HealthcheckRequest { 35 | } 36 | 37 | // Account request. 38 | message AccountRequest { 39 | } 40 | 41 | // A user with additional account details. Always the current user. 42 | message AccountResponse { 43 | // The user object. 44 | User user = 1; 45 | // The user's wallet data. 46 | string wallet = 2; 47 | // The email address of the user. 48 | string email = 3; 49 | // The devices which belong to the user's account. 50 | repeated AccountDevice devices = 4; 51 | // The custom id in the user's account. 52 | string custom_id = 5; 53 | // The UNIX time (for gRPC clients) or ISO string (for REST clients) when the user's email was verified. 54 | google.protobuf.Timestamp verify_time = 6; 55 | // The UNIX time (for gRPC clients) or ISO string (for REST clients) when the user's account was disabled/banned. 56 | google.protobuf.Timestamp disable_time = 7; 57 | } 58 | 59 | // Obtain a new authentication token using a refresh token. 60 | message AccountRefresh { 61 | // Refresh token. 62 | string token = 1; 63 | // Extra information that will be bundled in the session token. 64 | map vars = 2; 65 | } 66 | 67 | // Send a Apple Sign In token to the server. Used with authenticate/link/unlink. 68 | message AccountApple { 69 | // The ID token received from Apple to validate. 70 | string token = 1; 71 | // Extra information that will be bundled in the session token. 72 | map vars = 2; 73 | } 74 | 75 | // Send a custom ID to the server. Used with authenticate/link/unlink. 76 | message AccountCustom { 77 | // A custom identifier. 78 | string id = 1; 79 | // Extra information that will be bundled in the session token. 80 | map vars = 2; 81 | } 82 | 83 | // Send a device to the server. Used with authenticate/link/unlink and user. 84 | message AccountDevice { 85 | // A device identifier. Should be obtained by a platform-specific device API. 86 | string id = 1; 87 | // Extra information that will be bundled in the session token. 88 | map vars = 2; 89 | } 90 | 91 | // Send an email with password to the server. Used with authenticate/link/unlink. 92 | message AccountEmail { 93 | // A valid RFC-5322 email address. 94 | string email = 1; 95 | // A password for the user account. 96 | string password = 2; // Ignored with unlink operations. 97 | // Extra information that will be bundled in the session token. 98 | map vars = 3; 99 | } 100 | 101 | // Send a Facebook token to the server. Used with authenticate/link/unlink. 102 | message AccountFacebook { 103 | // The OAuth token received from Facebook to access their profile API. 104 | string token = 1; 105 | // Extra information that will be bundled in the session token. 106 | map vars = 2; 107 | } 108 | 109 | // Send a Facebook Instant Game token to the server. Used with authenticate/link/unlink. 110 | message AccountFacebookInstantGame { 111 | // The OAuth token received from a Facebook Instant Game that may be decoded with the Application Secret (must be available with the nakama configuration) 112 | string signed_player_info = 1; 113 | // Extra information that will be bundled in the session token. 114 | map vars = 2; 115 | } 116 | 117 | // Send Apple's Game Center account credentials to the server. Used with authenticate/link/unlink. 118 | message AccountGameCenter { 119 | // https://developer.apple.com/documentation/gamekit/gklocalplayer/1515407-generateidentityverificationsign 120 | 121 | // Player ID (generated by GameCenter). 122 | string player_id = 1; 123 | // Bundle ID (generated by GameCenter). 124 | string bundle_id = 2; 125 | // Time since UNIX epoch when the signature was created. 126 | int64 timestamp_seconds = 3; 127 | // A random "NSString" used to compute the hash and keep it randomized. 128 | string salt = 4; 129 | // The verification signature data generated. 130 | string signature = 5; 131 | // The URL for the public encryption key. 132 | string public_key_url = 6; 133 | // Extra information that will be bundled in the session token. 134 | map vars = 7; 135 | } 136 | 137 | // Send a Google token to the server. Used with authenticate/link/unlink. 138 | message AccountGoogle { 139 | // The OAuth token received from Google to access their profile API. 140 | string token = 1; 141 | // Extra information that will be bundled in the session token. 142 | map vars = 2; 143 | } 144 | 145 | // Send a Steam token to the server. Used with authenticate/link/unlink. 146 | message AccountSteam { 147 | // The account token received from Steam to access their profile API. 148 | string token = 1; 149 | // Extra information that will be bundled in the session token. 150 | map vars = 2; 151 | } 152 | 153 | // Add one or more friends to the current user. 154 | message AddFriendsRequest { 155 | // The account id of a user. 156 | repeated string ids = 1; 157 | // The account username of a user. 158 | repeated string usernames = 2; 159 | } 160 | 161 | // Add users to a group. 162 | message AddGroupUsersRequest { 163 | // The group to add users to. 164 | string group_id = 1; 165 | // The users to add. 166 | repeated string user_ids = 2; 167 | } 168 | 169 | // Authenticate against the server with a refresh token. 170 | message SessionRefreshRequest { 171 | // Refresh token. 172 | string token = 1; 173 | // Extra information that will be bundled in the session token. 174 | map vars = 2; 175 | } 176 | 177 | // Log out a session, invalidate a refresh token, or log out all sessions/refresh tokens for a user. 178 | message SessionLogoutRequest { 179 | // Session token to log out. 180 | string token = 1; 181 | // Refresh token to invalidate. 182 | string refresh_token = 2; 183 | } 184 | 185 | // Authenticate against the server with Apple Sign In. 186 | message AuthenticateAppleRequest { 187 | // The Apple account details. 188 | AccountApple account = 1; 189 | // Register the account if the user does not already exist. 190 | google.protobuf.BoolValue create = 2; 191 | // Set the username on the account at register. Must be unique. 192 | string username = 3; 193 | } 194 | 195 | // Authenticate against the server with a custom ID. 196 | message AuthenticateCustomRequest { 197 | // The custom account details. 198 | AccountCustom account = 1; 199 | // Register the account if the user does not already exist. 200 | google.protobuf.BoolValue create = 2; 201 | // Set the username on the account at register. Must be unique. 202 | string username = 3; 203 | } 204 | 205 | // Authenticate against the server with a device ID. 206 | message AuthenticateDeviceRequest { 207 | // The device account details. 208 | AccountDevice account = 1; 209 | // Register the account if the user does not already exist. 210 | google.protobuf.BoolValue create = 2; 211 | // Set the username on the account at register. Must be unique. 212 | string username = 3; 213 | } 214 | 215 | // Authenticate against the server with email+password. 216 | message AuthenticateEmailRequest { 217 | // The email account details. 218 | AccountEmail account = 1; 219 | // Register the account if the user does not already exist. 220 | google.protobuf.BoolValue create = 2; 221 | // Set the username on the account at register. Must be unique. 222 | string username = 3; 223 | } 224 | 225 | // Authenticate against the server with Facebook. 226 | message AuthenticateFacebookRequest { 227 | // The Facebook account details. 228 | AccountFacebook account = 1; 229 | // Register the account if the user does not already exist. 230 | google.protobuf.BoolValue create = 2; 231 | // Set the username on the account at register. Must be unique. 232 | string username = 3; 233 | // Import Facebook friends for the user. 234 | google.protobuf.BoolValue sync = 4; 235 | } 236 | 237 | // Authenticate against the server with Facebook Instant Game token. 238 | message AuthenticateFacebookInstantGameRequest { 239 | // The Facebook Instant Game account details. 240 | AccountFacebookInstantGame account = 1; 241 | // Register the account if the user does not already exist. 242 | google.protobuf.BoolValue create = 2; 243 | // Set the username on the account at register. Must be unique. 244 | string username = 3; 245 | } 246 | 247 | // Authenticate against the server with Apple's Game Center. 248 | message AuthenticateGameCenterRequest { 249 | // The Game Center account details. 250 | AccountGameCenter account = 1; 251 | // Register the account if the user does not already exist. 252 | google.protobuf.BoolValue create = 2; 253 | // Set the username on the account at register. Must be unique. 254 | string username = 3; 255 | } 256 | 257 | // Authenticate against the server with Google. 258 | message AuthenticateGoogleRequest { 259 | // The Google account details. 260 | AccountGoogle account = 1; 261 | // Register the account if the user does not already exist. 262 | google.protobuf.BoolValue create = 2; 263 | // Set the username on the account at register. Must be unique. 264 | string username = 3; 265 | } 266 | 267 | // Authenticate against the server with Steam. 268 | message AuthenticateSteamRequest { 269 | // The Steam account details. 270 | AccountSteam account = 1; 271 | // Register the account if the user does not already exist. 272 | google.protobuf.BoolValue create = 2; 273 | // Set the username on the account at register. Must be unique. 274 | string username = 3; 275 | // Import Steam friends for the user. 276 | google.protobuf.BoolValue sync = 4; 277 | } 278 | 279 | // Ban users from a group. 280 | message BanGroupUsersRequest { 281 | // The group to ban users from. 282 | string group_id = 1; 283 | // The users to ban. 284 | repeated string user_ids = 2; 285 | } 286 | 287 | // Block one or more friends for the current user. 288 | message BlockFriendsRequest { 289 | // The account id of a user. 290 | repeated string ids = 1; 291 | // The account username of a user. 292 | repeated string usernames = 2; 293 | } 294 | 295 | // A message sent on a channel. 296 | message ChannelMessage { 297 | // The channel this message belongs to. 298 | string channel_id = 1; 299 | // The unique ID of this message. 300 | string message_id = 2; 301 | // The code representing a message type or category. 302 | google.protobuf.Int32Value code = 3; 303 | // Message sender, usually a user ID. 304 | string sender_id = 4; 305 | // The username of the message sender, if any. 306 | string username = 5; 307 | // The content payload. 308 | string content = 6; 309 | // The UNIX time (for gRPC clients) or ISO string (for REST clients) when the message was created. 310 | google.protobuf.Timestamp create_time = 7; 311 | // The UNIX time (for gRPC clients) or ISO string (for REST clients) when the message was last updated. 312 | google.protobuf.Timestamp update_time = 8; 313 | // True if the message was persisted to the channel's history, false otherwise. 314 | google.protobuf.BoolValue persistent = 9; 315 | // The name of the chat room, or an empty string if this message was not sent through a chat room. 316 | string room_name = 10; 317 | // The ID of the group, or an empty string if this message was not sent through a group channel. 318 | string group_id = 11; 319 | // The ID of the first DM user, or an empty string if this message was not sent through a DM chat. 320 | string user_id_one = 12; 321 | // The ID of the second DM user, or an empty string if this message was not sent through a DM chat. 322 | string user_id_two = 13; 323 | } 324 | 325 | // A list of channel messages, usually a result of a list operation. 326 | message ChannelMessagesResponse { 327 | // A list of messages. 328 | repeated ChannelMessage messages = 1; 329 | // The cursor to send when retrieving the next page, if any. 330 | string next_cursor = 2; 331 | // The cursor to send when retrieving the previous page, if any. 332 | string prev_cursor = 3; 333 | // Cacheable cursor to list newer messages. Durable and designed to be stored, unlike next/prev cursors. 334 | string cacheable_cursor = 4; 335 | } 336 | 337 | // Create a group with the current user as owner. 338 | message CreateGroupRequest { 339 | // A unique name for the group. 340 | string name = 1; 341 | // A description for the group. 342 | string description = 2; 343 | // The language expected to be a tag which follows the BCP-47 spec. 344 | string lang_tag = 3; 345 | // A URL for an avatar image. 346 | string avatar_url = 4; 347 | // Mark a group as open or not where only admins can accept members. 348 | bool open = 5; 349 | // Maximum number of group members. 350 | int32 max_count = 6; 351 | } 352 | 353 | // Delete one or more friends for the current user. 354 | message DeleteFriendsRequest { 355 | // The account id of a user. 356 | repeated string ids = 1; 357 | // The account username of a user. 358 | repeated string usernames = 2; 359 | } 360 | 361 | // Delete a group the user has access to. 362 | message DeleteGroupRequest { 363 | // The id of a group. 364 | string group_id = 1; 365 | } 366 | 367 | // Delete a leaderboard record. 368 | message DeleteLeaderboardRecordRequest { 369 | // The leaderboard ID to delete from. 370 | string leaderboard_id = 1; 371 | } 372 | 373 | // Delete one or more notifications for the current user. 374 | message DeleteNotificationsRequest { 375 | // The id of notifications. 376 | repeated string ids = 1; 377 | } 378 | 379 | // Delete a leaderboard record. 380 | message DeleteTournamentRecordRequest { 381 | // The tournament ID to delete from. 382 | string tournament_id = 1; 383 | } 384 | 385 | // Storage objects to delete. 386 | message DeleteStorageObjectId { 387 | // The collection which stores the object. 388 | string collection = 1; 389 | // The key of the object within the collection. 390 | string key = 2; 391 | // The version hash of the object. 392 | string version = 3; 393 | } 394 | 395 | // Batch delete storage objects. 396 | message DeleteStorageObjectsRequest { 397 | // Batch of storage objects. 398 | repeated DeleteStorageObjectId object_ids = 1; 399 | } 400 | 401 | // Represents an event to be passed through the server to registered event handlers. 402 | message EventRequest { 403 | // An event name, type, category, or identifier. 404 | string name = 1; 405 | // Arbitrary event property values. 406 | map properties = 2; 407 | // The time when the event was triggered. 408 | google.protobuf.Timestamp timestamp = 3; 409 | // True if the event came directly from a client call, false otherwise. 410 | bool external = 4; 411 | } 412 | 413 | // The friendship status. 414 | enum FriendState { 415 | // The user is a friend of the current user. 416 | FRIEND = 0; 417 | // The current user has sent an invite to the user. 418 | INVITE_SENT = 1; 419 | // The current user has received an invite from this user. 420 | INVITE_RECEIVED = 2; 421 | // The current user has blocked this user. 422 | BLOCKED = 3; 423 | } 424 | 425 | // A friend of a user. 426 | message Friend { 427 | // The user object. 428 | User user = 1; 429 | // The friend status. -- use enum FriendState 430 | google.protobuf.Int32Value state = 2; // one of "Friend.State". 431 | // Time of the latest relationship update. 432 | google.protobuf.Timestamp update_time = 3; 433 | } 434 | 435 | // A collection of zero or more friends of the user. 436 | message FriendsResponse { 437 | // The Friend objects. 438 | repeated Friend friends = 1; 439 | // Cursor for the next page of results, if any. 440 | string cursor = 2; 441 | } 442 | 443 | // Fetch a batch of zero or more users from the server. 444 | message UsersRequest { 445 | // The account id of a user. 446 | repeated string ids = 1; 447 | // The account username of a user. 448 | repeated string usernames = 2; 449 | // The Facebook ID of a user. 450 | repeated string facebook_ids = 3; 451 | } 452 | 453 | // Fetch a subscription by product id. 454 | message SubscriptionRequest { 455 | // Product id of the subscription 456 | string product_id = 1; 457 | } 458 | 459 | // A group in the server. 460 | message Group { 461 | // The id of a group. 462 | string id = 1; 463 | // The id of the user who created the group. 464 | string creator_id = 2; 465 | // The unique name of the group. 466 | string name = 3; 467 | // A description for the group. 468 | string description = 4; 469 | // The language expected to be a tag which follows the BCP-47 spec. 470 | string lang_tag = 5; 471 | // Additional information stored as a JSON object. 472 | string metadata = 6; 473 | // A URL for an avatar image. 474 | string avatar_url = 7; 475 | // Anyone can join open groups, otherwise only admins can accept members. 476 | google.protobuf.BoolValue open = 8; 477 | // The current count of all members in the group. 478 | int32 edge_count = 9; 479 | // The maximum number of members allowed. 480 | int32 max_count = 10; 481 | // The UNIX time (for gRPC clients) or ISO string (for REST clients) when the group was created. 482 | google.protobuf.Timestamp create_time = 11; 483 | // The UNIX time (for gRPC clients) or ISO string (for REST clients) when the group was last updated. 484 | google.protobuf.Timestamp update_time = 12; 485 | } 486 | 487 | // One or more groups returned from a listing operation. 488 | message GroupsResponse { 489 | // One or more groups. 490 | repeated Group groups = 1; 491 | // A cursor used to get the next page. 492 | string cursor = 2; 493 | } 494 | 495 | // The group role status. 496 | enum UserRoleState { 497 | // The user is a superadmin with full control of the group. 498 | SUPERADMIN = 0; 499 | // The user is an admin with additional privileges. 500 | ADMIN = 1; 501 | // The user is a regular member. 502 | MEMBER = 2; 503 | // The user has requested to join the group 504 | JOIN_REQUEST = 3; 505 | } 506 | 507 | // A single user-role pair. 508 | message GroupUser { 509 | // User. 510 | User user = 1; 511 | // Their relationship to the group. -- use enum UserRoleState 512 | google.protobuf.Int32Value state = 2; 513 | } 514 | 515 | // A list of users belonging to a group, along with their role. 516 | message GroupUsersResponse { 517 | // User-role pairs for a group. 518 | repeated GroupUser group_users = 1; 519 | // Cursor for the next page of results, if any. 520 | string cursor = 2; 521 | } 522 | 523 | // Import Facebook friends into the current user's account. 524 | message ImportFacebookFriendsRequest { 525 | // The Facebook account details. 526 | AccountFacebook account = 1; 527 | // Reset the current user's friends list. 528 | google.protobuf.BoolValue reset = 2; 529 | } 530 | 531 | // Import Facebook friends into the current user's account. 532 | message ImportSteamFriendsRequest { 533 | // The Facebook account details. 534 | AccountSteam account = 1; 535 | // Reset the current user's friends list. 536 | google.protobuf.BoolValue reset = 2; 537 | } 538 | 539 | // Immediately join an open group, or request to join a closed one. 540 | message JoinGroupRequest { 541 | // The group ID to join. The group must already exist. 542 | string group_id = 1; 543 | } 544 | 545 | // The request to join a tournament. 546 | message JoinTournamentRequest { 547 | // The ID of the tournament to join. The tournament must already exist. 548 | string tournament_id = 1; 549 | } 550 | 551 | // Kick a set of users from a group. 552 | message KickGroupUsersRequest { 553 | // The group ID to kick from. 554 | string group_id = 1; 555 | // The users to kick. 556 | repeated string user_ids = 2; 557 | } 558 | 559 | // A leaderboard on the server. 560 | message Leaderboard { 561 | // The ID of the leaderboard. 562 | string id = 1; 563 | // ASC(0) or DESC(1) sort mode of scores in the leaderboard. 564 | uint32 sort_order = 2; 565 | // BEST, SET, INCREMENT or DECREMENT operator mode of the leaderboard. 566 | OpType operator = 3; 567 | // The UNIX time when the leaderboard was previously reset. A computed value. 568 | uint32 prev_reset = 4; 569 | // The UNIX time when the leaderboard is next playable. A computed value. 570 | uint32 next_reset = 5; 571 | // Additional information stored as a JSON object. 572 | string metadata = 6; 573 | // The UNIX time (for gRPC clients) or ISO string (for REST clients) when the leaderboard was created. 574 | google.protobuf.Timestamp create_time = 7; 575 | // Whether the leaderboard was created authoritatively or not. 576 | bool authoritative = 8; 577 | } 578 | 579 | // A list of leaderboards 580 | message LeaderboardsResponse { 581 | // The list of leaderboards returned. 582 | repeated Leaderboard leaderboards = 1; 583 | // A pagination cursor (optional). 584 | string cursor = 2; 585 | } 586 | 587 | // Represents a complete leaderboard record with all scores and associated metadata. 588 | message LeaderboardRecord { 589 | // The ID of the leaderboard this score belongs to. 590 | string leaderboard_id = 1; 591 | // The ID of the score owner, usually a user or group. 592 | string owner_id = 2; 593 | // The username of the score owner, if the owner is a user. 594 | google.protobuf.StringValue username = 3; 595 | // The score value. 596 | int64 score = 4; 597 | // An optional subscore value. 598 | int64 subscore = 5; 599 | // The number of submissions to this score record. 600 | int32 num_score = 6; 601 | // Metadata. 602 | string metadata = 7; 603 | // The UNIX time (for gRPC clients) or ISO string (for REST clients) when the leaderboard record was created. 604 | google.protobuf.Timestamp create_time = 8; 605 | // The UNIX time (for gRPC clients) or ISO string (for REST clients) when the leaderboard record was updated. 606 | google.protobuf.Timestamp update_time = 9; 607 | // The UNIX time (for gRPC clients) or ISO string (for REST clients) when the leaderboard record expires. 608 | google.protobuf.Timestamp expiry_time = 10; 609 | // The rank of this record. 610 | int64 rank = 11; 611 | // The maximum number of score updates allowed by the owner. 612 | uint32 max_num_score = 12; 613 | } 614 | 615 | // A set of leaderboard records, may be part of a leaderboard records page or a batch of individual records. 616 | message LeaderboardRecordsResponse { 617 | // A list of leaderboard records. 618 | repeated LeaderboardRecord records = 1; 619 | // A batched set of leaderboard records belonging to specified owners. 620 | repeated LeaderboardRecord owner_records = 2; 621 | // The cursor to send when retrieving the next page, if any. 622 | string next_cursor = 3; 623 | // The cursor to send when retrieving the previous page, if any. 624 | string prev_cursor = 4; 625 | } 626 | 627 | // Leave a group. 628 | message LeaveGroupRequest { 629 | // The group ID to leave. 630 | string group_id = 1; 631 | } 632 | 633 | // Link Facebook to the current user's account. 634 | message LinkFacebookRequest { 635 | // The Facebook account details. 636 | AccountFacebook account = 1; 637 | // Import Facebook friends for the user. 638 | google.protobuf.BoolValue sync = 2; 639 | } 640 | 641 | // Link Steam to the current user's account. 642 | message LinkSteamRequest { 643 | // The Facebook account details. 644 | AccountSteam account = 1; 645 | // Import Steam friends for the user. 646 | google.protobuf.BoolValue sync = 2; 647 | } 648 | 649 | // List a channel's message history. 650 | message ChannelMessagesRequest { 651 | // The channel ID to list from. 652 | string channel_id = 1; 653 | // Max number of records to return. Between 1 and 100. 654 | google.protobuf.Int32Value limit = 2; 655 | // True if listing should be older messages to newer, false if reverse. 656 | google.protobuf.BoolValue forward = 3; 657 | // A pagination cursor, if any. 658 | string cursor = 4; 659 | } 660 | 661 | // List friends for a user. 662 | message FriendsRequest { 663 | // Max number of records to return. Between 1 and 100. 664 | google.protobuf.Int32Value limit = 1; 665 | // The friend state to list. -- use enum UserRoleState 666 | google.protobuf.Int32Value state = 2; 667 | // An optional next page cursor. 668 | string cursor = 3; 669 | } 670 | 671 | // List groups based on given filters. 672 | message GroupsRequest { 673 | // List groups that contain this value in their names. 674 | string name = 1; 675 | // Optional pagination cursor. 676 | string cursor = 2; 677 | // Max number of groups to return. Between 1 and 100. 678 | google.protobuf.Int32Value limit = 3; 679 | // Language tag filter 680 | string lang_tag = 4; 681 | // Number of group members 682 | google.protobuf.Int32Value members = 5; 683 | // Optional Open/Closed filter. 684 | google.protobuf.BoolValue open = 6; 685 | } 686 | 687 | // List all users that are part of a group. 688 | message GroupUsersRequest { 689 | // The group ID to list from. 690 | string group_id = 1; 691 | // Max number of records to return. Between 1 and 100. 692 | google.protobuf.Int32Value limit = 2; 693 | // The group user state to list. -- use enum UserRoleState 694 | google.protobuf.Int32Value state = 3; 695 | // An optional next page cursor. 696 | string cursor = 4; 697 | } 698 | 699 | // List leaerboard records from a given leaderboard around the owner. 700 | message LeaderboardRecordsAroundOwnerRequest { 701 | // The ID of the tournament to list for. 702 | string leaderboard_id = 1; 703 | // Max number of records to return. Between 1 and 100. 704 | google.protobuf.UInt32Value limit = 2; 705 | // The owner to retrieve records around. 706 | string owner_id = 3; 707 | // Expiry in seconds (since epoch) to begin fetching records from. 708 | google.protobuf.Int64Value expiry = 4; 709 | // A next or previous page cursor. 710 | string cursor = 5; 711 | } 712 | 713 | // List leaderboard records from a given leaderboard. 714 | message LeaderboardRecordsRequest { 715 | // The ID of the leaderboard to list for. 716 | string leaderboard_id = 1; 717 | // One or more owners to retrieve records for. 718 | repeated string owner_ids = 2; 719 | // Max number of records to return. Between 1 and 100. 720 | google.protobuf.Int32Value limit = 3; 721 | // A next or previous page cursor. 722 | string cursor = 4; 723 | // Expiry in seconds (since epoch) to begin fetching records from. Optional. 0 means from current time. 724 | google.protobuf.Int64Value expiry = 5; 725 | } 726 | 727 | // List realtime matches. 728 | message MatchesRequest { 729 | // Limit the number of returned matches. 730 | google.protobuf.Int32Value limit = 1; 731 | // Authoritative or relayed matches. 732 | google.protobuf.BoolValue authoritative = 2; 733 | // Label filter. 734 | google.protobuf.StringValue label = 3; 735 | // Minimum user count. 736 | google.protobuf.Int32Value min_size = 4; 737 | // Maximum user count. 738 | google.protobuf.Int32Value max_size = 5; 739 | // Arbitrary label query. 740 | google.protobuf.StringValue query = 6; 741 | } 742 | 743 | // Get a list of unexpired notifications. 744 | message NotificationsRequest { 745 | // The number of notifications to get. Between 1 and 100. 746 | google.protobuf.Int32Value limit = 1; 747 | // A cursor to page through notifications. May be cached by clients to get from point in time forwards. 748 | string cacheable_cursor = 2; // value from NotificationList.cacheable_cursor. 749 | } 750 | 751 | // List publicly readable storage objects in a given collection. 752 | message StorageObjectsRequest { 753 | // ID of the user. 754 | string user_id = 1; 755 | // The collection which stores the object. 756 | string collection = 2; 757 | // The number of storage objects to list. Between 1 and 100. 758 | google.protobuf.Int32Value limit = 3; 759 | // The cursor to page through results from. 760 | string cursor = 4; // value from StorageObjectList.cursor. 761 | } 762 | 763 | // List user subscriptions. 764 | message SubscriptionsRequest { 765 | // Max number of results per page 766 | google.protobuf.Int32Value limit = 1; 767 | // Cursor to retrieve a page of records from 768 | string cursor = 2; 769 | } 770 | 771 | // List tournament records from a given tournament around the owner. 772 | message TournamentRecordsAroundOwnerRequest { 773 | // The ID of the tournament to list for. 774 | string tournament_id = 1; 775 | // Max number of records to return. Between 1 and 100. 776 | google.protobuf.UInt32Value limit = 2; 777 | // The owner to retrieve records around. 778 | string owner_id = 3; 779 | // Expiry in seconds (since epoch) to begin fetching records from. 780 | google.protobuf.Int64Value expiry = 4; 781 | // A next or previous page cursor. 782 | string cursor = 5; 783 | } 784 | 785 | // List tournament records from a given tournament. 786 | message TournamentRecordsRequest { 787 | // The ID of the tournament to list for. 788 | string tournament_id = 1; 789 | // One or more owners to retrieve records for. 790 | repeated string owner_ids = 2; 791 | // Max number of records to return. Between 1 and 100. 792 | google.protobuf.Int32Value limit = 3; 793 | // A next or previous page cursor. 794 | string cursor = 4; 795 | // Expiry in seconds (since epoch) to begin fetching records from. 796 | google.protobuf.Int64Value expiry = 5; 797 | } 798 | 799 | // List active/upcoming tournaments based on given filters. 800 | message TournamentsRequest { 801 | // The start of the categories to include. Defaults to 0. 802 | google.protobuf.UInt32Value category_start = 1; 803 | // The end of the categories to include. Defaults to 128. 804 | google.protobuf.UInt32Value category_end = 2; 805 | // The start time for tournaments. Defaults to epoch. 806 | google.protobuf.UInt32Value start_time = 3; 807 | // The end time for tournaments. Defaults to +1 year from current Unix time. 808 | google.protobuf.UInt32Value end_time = 4; 809 | // Max number of records to return. Between 1 and 100. 810 | google.protobuf.Int32Value limit = 6; 811 | // A next page cursor for listings (optional). 812 | string cursor = 8; 813 | } 814 | 815 | // List the groups a user is part of, and their relationship to each. 816 | message UserGroupsRequest { 817 | // ID of the user. 818 | string user_id = 1; 819 | // Max number of records to return. Between 1 and 100. 820 | google.protobuf.Int32Value limit = 2; 821 | // The user group state to list. -- use enum UserRoleState 822 | google.protobuf.Int32Value state = 3; 823 | // An optional next page cursor. 824 | string cursor = 4; 825 | } 826 | 827 | // Represents a realtime match. 828 | message Match { 829 | // The ID of the match, can be used to join. 830 | string match_id = 1; 831 | // True if it's an server-managed authoritative match, false otherwise. 832 | bool authoritative = 2; 833 | // Match label, if any. 834 | google.protobuf.StringValue label = 3; 835 | // Current number of users in the match. 836 | int32 size = 4; 837 | // Tick Rate 838 | int32 tick_rate = 5; 839 | // Handler name 840 | string handler_name = 6; 841 | } 842 | 843 | // A list of realtime matches. 844 | message MatchesResponse { 845 | // A number of matches corresponding to a list operation. 846 | repeated Match matches = 1; 847 | } 848 | 849 | // A notification in the server. 850 | message Notification { 851 | // ID of the Notification. 852 | string id = 1; 853 | // Subject of the notification. 854 | string subject = 2; 855 | // Content of the notification in JSON. 856 | string content = 3; 857 | // Category code for this notification. 858 | int32 code = 4; 859 | // ID of the sender, if a user. Otherwise 'null'. 860 | string sender_id = 5; 861 | // The UNIX time (for gRPC clients) or ISO string (for REST clients) when the notification was created. 862 | google.protobuf.Timestamp create_time = 6; 863 | // True if this notification was persisted to the database. 864 | bool persistent = 7; 865 | } 866 | 867 | // A collection of zero or more notifications. 868 | message NotificationsResponse { 869 | // Collection of notifications. 870 | repeated Notification notifications = 1; 871 | // Use this cursor to paginate notifications. Cache this to catch up to new notifications. 872 | string cacheable_cursor = 2; 873 | } 874 | 875 | // Promote a set of users in a group to the next role up. 876 | message PromoteGroupUsersRequest { 877 | // The group ID to promote in. 878 | string group_id = 1; 879 | // The users to promote. 880 | repeated string user_ids = 2; 881 | } 882 | 883 | // Demote a set of users in a group to the next role down. 884 | message DemoteGroupUsersRequest { 885 | // The group ID to demote in. 886 | string group_id = 1; 887 | // The users to demote. 888 | repeated string user_ids = 2; 889 | } 890 | 891 | // Storage objects to get. 892 | message ReadStorageObjectId { 893 | // The collection which stores the object. 894 | string collection = 1; 895 | // The key of the object within the collection. 896 | string key = 2; 897 | // The user owner of the object. 898 | string user_id = 3; 899 | } 900 | 901 | // Batch get storage objects. 902 | message ReadStorageObjectsRequest { 903 | // Batch of storage objects. 904 | repeated ReadStorageObjectId object_ids = 1; 905 | } 906 | 907 | // Execute an Lua function on the server. 908 | message RpcMsg { 909 | // The identifier of the function. 910 | string id = 1; 911 | // The payload of the function which must be a JSON object. 912 | string payload = 2; 913 | // The authentication key used when executed as a non-client HTTP request. 914 | string http_key = 3; 915 | } 916 | 917 | // A user's session used to authenticate messages. 918 | message SessionResponse { 919 | // True if the corresponding account was just created, false otherwise. 920 | bool created = 1; 921 | // Authentication credentials. 922 | string token = 2; 923 | // Refresh token that can be used for session token renewal. 924 | string refresh_token = 3; 925 | } 926 | 927 | // An object within the storage engine. 928 | message StorageObject { 929 | // The collection which stores the object. 930 | string collection = 1; 931 | // The key of the object within the collection. 932 | string key = 2; 933 | // The user owner of the object. 934 | string user_id = 3; 935 | // The value of the object. 936 | string value = 4; 937 | // The version hash of the object. 938 | string version = 5; 939 | // The read access permissions for the object. 940 | int32 permission_read = 6; 941 | // The write access permissions for the object. 942 | int32 permission_write = 7; 943 | // The UNIX time (for gRPC clients) or ISO string (for REST clients) when the object was created. 944 | google.protobuf.Timestamp create_time = 8; 945 | // The UNIX time (for gRPC clients) or ISO string (for REST clients) when the object was last updated. 946 | google.protobuf.Timestamp update_time = 9; 947 | } 948 | 949 | // A storage acknowledgement. 950 | message StorageObjectAck { 951 | // The collection which stores the object. 952 | string collection = 1; 953 | // The key of the object within the collection. 954 | string key = 2; 955 | // The version hash of the object. 956 | string version = 3; 957 | // The owner of the object. 958 | string user_id = 4; 959 | } 960 | 961 | // Batch of acknowledgements for the storage object write. 962 | message WriteStorageObjectsResponse { 963 | // Batch of storage write acknowledgements. 964 | repeated StorageObjectAck acks = 1; 965 | } 966 | 967 | // Batch of storage objects. 968 | message ReadStorageObjectsResponse { 969 | // The batch of storage objects. 970 | repeated StorageObject objects = 1; 971 | } 972 | 973 | // List of storage objects. 974 | message StorageObjectsResponse { 975 | // The list of storage objects. 976 | repeated StorageObject objects = 1; 977 | // The cursor for the next page of results, if any. 978 | string cursor = 2; 979 | } 980 | 981 | // A tournament on the server. 982 | message Tournament { 983 | // The ID of the tournament. 984 | string id = 1; 985 | // The title for the tournament. 986 | string title = 2; 987 | // The description of the tournament. May be blank. 988 | string description = 3; 989 | // The category of the tournament. e.g. "vip" could be category 1. 990 | uint32 category = 4; 991 | // ASC (0) or DESC (1) sort mode of scores in the tournament. 992 | uint32 sort_order = 5; 993 | // The current number of players in the tournament. 994 | uint32 size = 6; 995 | // The maximum number of players for the tournament. 996 | uint32 max_size = 7; 997 | // The maximum score updates allowed per player for the current tournament. 998 | uint32 max_num_score = 8; 999 | // True if the tournament is active and can enter. A computed value. 1000 | bool can_enter = 9; 1001 | // The UNIX time when the tournament stops being active until next reset. A computed value. 1002 | uint32 end_active = 10; 1003 | // The UNIX time when the tournament is next playable. A computed value. 1004 | uint32 next_reset = 11; 1005 | // Additional information stored as a JSON object. 1006 | string metadata = 12; 1007 | // The UNIX time (for gRPC clients) or ISO string (for REST clients) when the tournament was created. 1008 | google.protobuf.Timestamp create_time = 13; 1009 | // The UNIX time (for gRPC clients) or ISO string (for REST clients) when the tournament will start. 1010 | google.protobuf.Timestamp start_time = 14; 1011 | // The UNIX time (for gRPC clients) or ISO string (for REST clients) when the tournament will be stopped. 1012 | google.protobuf.Timestamp end_time = 15; 1013 | // Duration of the tournament in seconds. 1014 | uint32 duration = 16; 1015 | // The UNIX time when the tournament start being active. A computed value. 1016 | uint32 start_active = 17; 1017 | // The UNIX time when the tournament was last reset. A computed value. 1018 | uint32 prev_reset = 18; 1019 | // Operator. 1020 | OpType operator = 19; 1021 | // Whether the leaderboard was created authoritatively or not. 1022 | bool authoritative = 20; 1023 | } 1024 | 1025 | // A list of tournaments. 1026 | message TournamentsResponse { 1027 | // The list of tournaments returned. 1028 | repeated Tournament tournaments = 1; 1029 | // A pagination cursor (optional). 1030 | string cursor = 2; 1031 | } 1032 | 1033 | // A set of tournament records which may be part of a tournament records page or a batch of individual records. 1034 | message TournamentRecordsResponse { 1035 | // A list of tournament records. 1036 | repeated LeaderboardRecord records = 1; 1037 | // A batched set of tournament records belonging to specified owners. 1038 | repeated LeaderboardRecord owner_records = 2; 1039 | // The cursor to send when retireving the next page (optional). 1040 | string next_cursor = 3; 1041 | // The cursor to send when retrieving the previous page (optional). 1042 | string prev_cursor = 4; 1043 | } 1044 | 1045 | // Update a user's account details. 1046 | message UpdateAccountRequest { 1047 | // The username of the user's account. 1048 | google.protobuf.StringValue username = 1; 1049 | // The display name of the user. 1050 | google.protobuf.StringValue display_name = 2; 1051 | // A URL for an avatar image. 1052 | google.protobuf.StringValue avatar_url = 3; 1053 | // The language expected to be a tag which follows the BCP-47 spec. 1054 | google.protobuf.StringValue lang_tag = 4; 1055 | // The location set by the user. 1056 | google.protobuf.StringValue location = 5; 1057 | // The timezone set by the user. 1058 | google.protobuf.StringValue timezone = 6; 1059 | } 1060 | 1061 | // Update fields in a given group. 1062 | message UpdateGroupRequest { 1063 | // The ID of the group to update. 1064 | string group_id = 1; 1065 | // Name. 1066 | google.protobuf.StringValue name = 2; 1067 | // Description string. 1068 | google.protobuf.StringValue description = 3; 1069 | // Lang tag. 1070 | google.protobuf.StringValue lang_tag = 4; 1071 | // Avatar URL. 1072 | google.protobuf.StringValue avatar_url = 5; 1073 | // Open is true if anyone should be allowed to join, or false if joins must be approved by a group admin. 1074 | google.protobuf.BoolValue open = 6; 1075 | } 1076 | 1077 | // A user in the server. 1078 | message User { 1079 | // The id of the user's account. 1080 | string id = 1; 1081 | // The username of the user's account. 1082 | string username = 2; 1083 | // The display name of the user. 1084 | string display_name = 3; 1085 | // A URL for an avatar image. 1086 | string avatar_url = 4; 1087 | // The language expected to be a tag which follows the BCP-47 spec. 1088 | string lang_tag = 5; 1089 | // The location set by the user. 1090 | string location = 6; 1091 | // The timezone set by the user. 1092 | string timezone = 7; 1093 | // Additional information stored as a JSON object. 1094 | string metadata = 8; 1095 | // The Facebook id in the user's account. 1096 | string facebook_id = 9; 1097 | // The Google id in the user's account. 1098 | string google_id = 10; 1099 | // The Apple Game Center in of the user's account. 1100 | string gamecenter_id = 11; 1101 | // The Steam id in the user's account. 1102 | string steam_id = 12; 1103 | // Indicates whether the user is currently online. 1104 | bool online = 13; 1105 | // Number of related edges to this user. 1106 | int32 edge_count = 14; 1107 | // The UNIX time (for gRPC clients) or ISO string (for REST clients) when the user was created. 1108 | google.protobuf.Timestamp create_time = 15; 1109 | // The UNIX time (for gRPC clients) or ISO string (for REST clients) when the user was last updated. 1110 | google.protobuf.Timestamp update_time = 16; 1111 | // The Facebook Instant Game ID in the user's account. 1112 | string facebook_instant_game_id = 17; 1113 | // The Apple Sign In ID in the user's account. 1114 | string apple_id = 18; 1115 | } 1116 | 1117 | // A single group-role pair. 1118 | message UserGroup { 1119 | // Group. 1120 | Group group = 1; 1121 | // The user's relationship to the group. -- use enum UserRoleState 1122 | google.protobuf.Int32Value state = 2; 1123 | } 1124 | 1125 | // A list of groups belonging to a user, along with the user's role in each group. 1126 | message UserGroupsResponse { 1127 | // Group-role pairs for a user. 1128 | repeated UserGroup user_groups = 1; 1129 | // Cursor for the next page of results, if any. 1130 | string cursor = 2; 1131 | } 1132 | 1133 | // A collection of zero or more users. 1134 | message UsersResponse { 1135 | // The User objects. 1136 | repeated User users = 1; 1137 | } 1138 | 1139 | // Apple IAP Purchases validation request 1140 | message ValidatePurchaseAppleRequest { 1141 | // Base64 encoded Apple receipt data payload. 1142 | string receipt = 1; 1143 | // Persist the purchase 1144 | google.protobuf.BoolValue persist = 2; 1145 | } 1146 | 1147 | // Apple Subscription validation request 1148 | message ValidateSubscriptionAppleRequest { 1149 | // Base64 encoded Apple receipt data payload. 1150 | string receipt = 1; 1151 | // Persist the subscription. 1152 | google.protobuf.BoolValue persist = 2; 1153 | } 1154 | 1155 | // Google IAP Purchase validation request 1156 | message ValidatePurchaseGoogleRequest { 1157 | // JSON encoded Google purchase payload. 1158 | string purchase = 1; 1159 | // Persist the purchase 1160 | google.protobuf.BoolValue persist = 2; 1161 | } 1162 | 1163 | // Google Subscription validation request 1164 | message ValidateSubscriptionGoogleRequest { 1165 | // JSON encoded Google purchase payload. 1166 | string receipt = 1; 1167 | // Persist the subscription. 1168 | google.protobuf.BoolValue persist = 2; 1169 | } 1170 | 1171 | // Huawei IAP Purchase validation request 1172 | message ValidatePurchaseHuaweiRequest { 1173 | // JSON encoded Huawei InAppPurchaseData. 1174 | string purchase = 1; 1175 | // InAppPurchaseData signature. 1176 | string signature = 2; 1177 | // Persist the purchase 1178 | google.protobuf.BoolValue persist = 3; 1179 | } 1180 | 1181 | // Validated Purchase stored by Nakama. 1182 | message ValidatedPurchase { 1183 | // Purchase User ID. 1184 | string user_id = 1; 1185 | // Purchase Product ID. 1186 | string product_id = 2; 1187 | // Purchase Transaction ID. 1188 | string transaction_id = 3; 1189 | // Store identifier 1190 | StoreProvider store = 4; 1191 | // Timestamp when the purchase was done. 1192 | google.protobuf.Timestamp purchase_time = 5; 1193 | // Timestamp when the receipt validation was stored in DB. 1194 | google.protobuf.Timestamp create_time = 6; 1195 | // Timestamp when the receipt validation was updated in DB. 1196 | google.protobuf.Timestamp update_time = 7; 1197 | // Timestamp when the purchase was refunded. Set to UNIX 1198 | google.protobuf.Timestamp refund_time = 8; 1199 | // Raw provider validation response. 1200 | string provider_response = 9; 1201 | // Whether the purchase was done in production or sandbox environment. 1202 | StoreEnvironment environment = 10; 1203 | // Whether the purchase had already been validated by Nakama before. 1204 | bool seen_before = 11; 1205 | } 1206 | 1207 | // Validate IAP response. 1208 | message ValidatePurchaseResponse { 1209 | // Newly seen validated purchases. 1210 | repeated ValidatedPurchase validated_purchases = 1; 1211 | } 1212 | 1213 | // Validate Subscription response. 1214 | message ValidateSubscriptionResponse { 1215 | ValidatedSubscription validated_subscription = 1; 1216 | } 1217 | 1218 | // Validation Provider, 1219 | enum StoreProvider { 1220 | // Apple App Store 1221 | APPLE_APP_STORE = 0; 1222 | // Google Play Store 1223 | GOOGLE_PLAY_STORE = 1; 1224 | // Huawei App Gallery 1225 | HUAWEI_APP_GALLERY = 2; 1226 | } 1227 | 1228 | // Environment where a purchase/subscription took place, 1229 | enum StoreEnvironment { 1230 | // Unknown environment. 1231 | UNKNOWN = 0; 1232 | // Sandbox/test environment. 1233 | SANDBOX = 1; 1234 | // Production environment. 1235 | PRODUCTION = 2; 1236 | } 1237 | 1238 | message ValidatedSubscription { 1239 | // Subscription User ID. 1240 | string user_id = 1; 1241 | // Purchase Product ID. 1242 | string product_id = 2; 1243 | // Purchase Original transaction ID (we only keep track of the original subscription, not subsequent renewals). 1244 | string original_transaction_id = 3; 1245 | // Store identifier 1246 | StoreProvider store = 4; 1247 | // UNIX Timestamp when the purchase was done. 1248 | google.protobuf.Timestamp purchase_time = 5; 1249 | // UNIX Timestamp when the receipt validation was stored in DB. 1250 | google.protobuf.Timestamp create_time = 6; 1251 | // UNIX Timestamp when the receipt validation was updated in DB. 1252 | google.protobuf.Timestamp update_time = 7; 1253 | // Whether the purchase was done in production or sandbox environment. 1254 | StoreEnvironment environment = 8; 1255 | // Subscription expiration time. The subscription can still be auto-renewed to extend the expiration time further. 1256 | google.protobuf.Timestamp expiry_time = 9; 1257 | // Subscription refund time. If this time is set, the subscription was refunded. 1258 | google.protobuf.Timestamp refund_time = 10; 1259 | // Raw provider validation response body. 1260 | string provider_response = 11; 1261 | // Raw provider notification body. 1262 | string provider_notification = 12; 1263 | // Whether the subscription is currently active or not. 1264 | bool active = 13; 1265 | } 1266 | 1267 | // A list of validated purchases stored by Nakama. 1268 | message PurchasesResponse { 1269 | // Stored validated purchases. 1270 | repeated ValidatedPurchase validated_purchases = 1; 1271 | // The cursor to send when retrieving the next page, if any. 1272 | string cursor = 2; 1273 | // The cursor to send when retrieving the previous page, if any. 1274 | string prev_cursor = 3; 1275 | } 1276 | 1277 | // A list of validated subscriptions stored by Nakama. 1278 | message SubscriptionsResponse { 1279 | // Stored validated subscriptions. 1280 | repeated ValidatedSubscription validated_subscriptions = 1; 1281 | // The cursor to send when retrieving the next page, if any. 1282 | string cursor = 2; 1283 | // The cursor to send when retrieving the previous page, if any. 1284 | string prev_cursor = 3; 1285 | } 1286 | 1287 | // Record values to write. 1288 | message LeaderboardRecordWrite { 1289 | // The score value to submit. 1290 | int64 score = 1; 1291 | // An optional secondary value. 1292 | int64 subscore = 2; 1293 | // Optional record metadata. 1294 | string metadata = 3; 1295 | // Operator override. 1296 | OpType operator = 4; 1297 | } 1298 | 1299 | // A request to submit a score to a leaderboard. 1300 | message WriteLeaderboardRecordRequest { 1301 | // The ID of the leaderboard to write to. 1302 | string leaderboard_id = 1; 1303 | // Record input. 1304 | LeaderboardRecordWrite record = 2; 1305 | } 1306 | 1307 | // The object to store. 1308 | message WriteStorageObject { 1309 | // The collection to store the object. 1310 | string collection = 1; 1311 | // The key for the object within the collection. 1312 | string key = 2; 1313 | // The value of the object. 1314 | string value = 3; 1315 | // The version hash of the object to check. Possible values are: ["", "*", "#hash#"]. 1316 | string version = 4; // if-match and if-none-match 1317 | // The read access permissions for the object. 1318 | google.protobuf.Int32Value permission_read = 5; 1319 | // The write access permissions for the object. 1320 | google.protobuf.Int32Value permission_write = 6; 1321 | } 1322 | 1323 | // Write objects to the storage engine. 1324 | message WriteStorageObjectsRequest { 1325 | // The objects to store on the server. 1326 | repeated WriteStorageObject objects = 1; 1327 | } 1328 | 1329 | // Record values to write. 1330 | message TournamentRecordWrite { 1331 | // The score value to submit. 1332 | int64 score = 1; 1333 | // An optional secondary value. 1334 | int64 subscore = 2; 1335 | // A JSON object of additional properties (optional). 1336 | string metadata = 3; 1337 | // Operator override. 1338 | OpType operator = 4; 1339 | } 1340 | 1341 | // A request to submit a score to a tournament. 1342 | message WriteTournamentRecordRequest { 1343 | // The tournament ID to write the record for. 1344 | string tournament_id = 1; 1345 | // Record input. 1346 | TournamentRecordWrite record = 2; 1347 | } 1348 | 1349 | // Operator that can be used to override the one set in the leaderboard. 1350 | enum OpType { 1351 | // Do not override the leaderboard operator. 1352 | NO_OVERRIDE = 0; 1353 | // Override the leaderboard operator with BEST. 1354 | BEST = 1; 1355 | // Override the leaderboard operator with SET. 1356 | SET = 2; 1357 | // Override the leaderboard operator with INCREMENT. 1358 | INCREMENT = 3; 1359 | // Override the leaderboard operator with DECREMENT. 1360 | DECREMENT = 4; 1361 | } 1362 | --------------------------------------------------------------------------------