Permission is hereby granted, free of charge, to any person obtaining a copy of
60 | this software and associated documentation files (the "Software"), to deal in
61 | the Software without restriction, including without limitation the rights to
62 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
63 | of the Software, and to permit persons to whom the Software is furnished to do
64 | so, subject to the following conditions:
65 |
The above copyright notice and this permission notice shall be included in all
66 | copies or substantial portions of the Software.
67 |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
68 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
69 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
70 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
71 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
72 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
73 | SOFTWARE.
74 |
152 | generated by LDoc 1.4.6
153 | Last updated 2021-11-05 15:42:41
154 |
155 |
156 |
157 |
158 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
7 |
8 | ## [Unreleased]
9 |
10 | ### Added
11 | - Added `socket.disconnect()`
12 |
13 | ### Fixed
14 | - Creating an exponentially increasing retry interval caused a Lua error
15 |
16 | ## [3.4.0] - 2024-09-16
17 | ### Added
18 | - Added support for Satori
19 |
20 | ## [3.3.0] - 2024-06-14
21 | ### Fixed
22 | - Fixed issue with wrong argument name for `nakama.rpc_func`
23 | - Updated several of the socket messages so that they no longer incorrectly wait for a response from the server.
24 |
25 | ## [3.2.0] - 2023-12-11
26 | ### Changed
27 | - Use native Defold json encode and decode functions
28 | - Updated gRPC API bindings to version 3.19.0
29 | - Updated Real-Time API bindings to verison 1.30.0
30 |
31 | ### Added
32 | - Added optional native b64 encode and decode using extension-crypt if it exists
33 |
34 | ## [3.1.0] - 2022-10-27
35 | ### Added
36 | - Added utility functions to store and restore tokens
37 | - Added a refresh token to the session table and functions to detect expired or soon to be expired tokens
38 | - Added global and per-request retries of failed requests
39 | - Added cancellation token for Rest API requests
40 | - Added `on_party_leader()` socket event
41 | - Added `socket.CHANNELTYPE_*` and `socket.ERROR_*` constants
42 | - Added updated Rest API definitions (in-app subscriptions)
43 |
44 | ## [3.0.3] - 2022-05-20
45 | ### Fixed
46 | - Fixed issue with incorrect match data property being used in `socket_send` function.
47 |
48 | ## [3.0.2] - 2022-05-12
49 | ### Changed
50 | - Allows optional parameters to be nil in socket functions
51 |
52 |
53 | ## [3.0.1] - 2022-04-11
54 | ### Fixed
55 | - Runtime error when an unhandled socket message is received (#43)
56 |
57 |
58 | ## [3.0.0] - 2022-04-08
59 | Please note that the Defold SDK version is not synchronised with the version of the Nakama server!
60 |
61 | ### Changed
62 | - [BREAKING] Major overhaul of the generated code and how it interacts with the Nakama APIs.
63 | - Socket creation and socket events have been moved to `nakama/socket.lua`. This includes sending events and adding socket event listeners.
64 | - Removed message creation functions in favor of including all message arguments in the functions sending the messages.
65 | - Added message functions to the client and socket instances. Compare `nakama.do_foo(client, ...)` and `client.do_foo(...)`. The old approach of passing the client or socket instance as the first argument still exists to help with backwards compatibility.
66 |
67 |
68 | ## [2.1.2] - 2021-09-29
69 | ### Fixed
70 | - Status follow and unfollow messages used the wrong argument name.
71 |
72 |
73 | ## [2.1.1] - 2021-08-09
74 | ### Fixed
75 | - Encoding of empty status update message.
76 |
77 |
78 | ## [2.1.0] - 2021-06-01
79 | ### Added
80 | - Generated new version of the API. New API functions: nakama.validate_purchase_apple(), nakama.validate_purchase_google(), nakama.validate_purchase_huawei(), nakama.session_logout(), nakama.write_tournament_record2(), nakama.import_steam_friends()
81 |
82 | ### Changed
83 | - Signatures for a few functions operating on user groups and friends.
84 |
85 |
86 | ## [2.0.0] - 2021-02-23
87 | ### Changed
88 | - Updated to the new native WebSocket extension for Defold (https://github.com/defold/extension-websocket). To use Nakama with Defold you now only need to add a dependency to the WebSocket extension.
89 |
90 | ### Fixed
91 | - HTTP requests handle HTTP status codes outside of the 200-299 range as errors. The general error handling based on the response from Nakama has also been improved.
92 | - Match create messages are encoded correctly when the message is empty.
93 | - [Issue 14](https://github.com/heroiclabs/nakama-defold/issues/14): Attempt to call global 'uri_encode' (a nil value)
94 | - Upgrade code generator to new Swagger format introduces with Nakama v.2.14.0
95 | - Do not use Lua default values for variables in `create_` methods to prevent data reset on backend
96 |
97 |
98 | ## [1.1.1] - 2020-06-30
99 | ### Fixed
100 | - Fixes issues with re-authentication (by dropping an existing bearer token when calling an authentication function)
101 |
102 |
103 | ## [1.1.0] - 2020-06-21
104 | ### Added
105 | - Support for encoding of match data (json+base64) using new utility module
106 |
107 | ### Fixed
108 | - Use of either http or https connection via `config.use_ssl`
109 |
110 |
111 | ## [1.0.1] - 2020-05-31
112 | ### Fixed
113 | - The default logging was not working
114 |
115 |
116 | ## [1.0.0] - 2020-05-25
117 | ### Added
118 | - First public release
119 |
--------------------------------------------------------------------------------
/docs/modules/nakama.engine.defold.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 | SDK API documentation
7 |
8 |
9 |
10 |
11 |
Licensed under the Apache License, Version 2.0 (the "License");
61 | you may not use this file except in compliance with the License.
62 | You may obtain a copy of the License at
63 |
http://www.apache.org/licenses/LICENSE-2.0
64 |
Unless required by applicable law or agreed to in writing, software
65 | distributed under the License is distributed on an "AS-IS" BASIS,
66 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
67 | See the License for the specific language governing permissions and
68 | limitations under the License.
69 |
see http://www.ietf.org/rfc/rfc4122.txt
70 |
Note that this is not a true version 4 (random) UUID. Since `os.time()` precision is only 1 second, it would be hard
71 | to guarantee spacial uniqueness when two hosts generate a uuid after being seeded during the same second. This
72 | is solved by using the node field from a version 1 UUID. It represents the mac address.
73 |
28-apr-2013 modified by Thijs Schreijer from the original [Rackspace code](https://github.com/kans/zirgo/blob/807250b1af6725bad4776c931c89a784c1e34db2/util/uuid.lua) as a generic Lua module.
74 | Regarding the above mention on `os.time()`; the modifications use the `socket.gettime()` function from LuaSocket
75 | if available and hence reduce that problem (provided LuaSocket has been loaded before uuid).
76 |
**6-nov-2015 Please take note of this issue**; [https://github.com/Mashape/kong/issues/478](https://github.com/Mashape/kong/issues/478)
77 | It demonstrates the problem of using time as a random seed. Specifically when used from multiple processes.
78 | So make sure to seed only once, application wide. And to not have multiple processes do that
79 | simultaneously (like nginx does for example).
80 |
18-jun-2020 modified by [@uncleNight](https://github.com/uncleNight) - dirty workaround for Defold compatibility:
81 | removed require() for 'math', 'os' and 'string' modules since Defold Lua runtime exports them globally, so
82 | requiring them breaks [bob](https://defold.com/manuals/bob/) builds.
83 |
114 | Creates a new uuid. Either provide a unique hex string, or make sure the
115 | random seed is properly set. The module table itself is a shortcut to this
116 | function, so `my_uuid = uuid.new()` equals `my_uuid = uuid()`.
117 |
For proper use there are 3 options;
118 |
1. first require `luasocket`, then call `uuid.seed()`, and request a uuid using no
119 | parameter, eg. `my_uuid = uuid()`
120 | 2. use `uuid` without `luasocket`, set a random seed using `uuid.randomseed(some_good_seed)`,
121 | and request a uuid using no parameter, eg. `my_uuid = uuid()`
122 | 3. use `uuid` without `luasocket`, and request a uuid using an unique hex string,
123 | eg. `my_uuid = uuid(my_networkcard_macaddress)`
124 |
125 |
126 |
127 |
Parameters:
128 |
129 |
hwaddr
130 | (optional) string containing a unique hex value (e.g.: `00:0c:29:69:41:c6`), to be used to compensate for the lesser `math_random()` function. Use a mac address for solid results. If omitted, a fully randomized uuid will be generated, but then you must ensure that the random seed is set properly!
131 |
local uuid = require("uuid")
145 | print("here's a new uuid: ",uuid())
146 |
147 |
148 |
149 |
150 |
151 | randomseed (seed)
152 |
153 |
154 | Improved randomseed function.
155 | Lua 5.1 and 5.2 both truncate the seed given if it exceeds the integer
156 | range. If this happens, the seed will be 0 or 1 and all randomness will
157 | be gone (each application run will generate the same sequence of random
158 | numbers in that case). This improved version drops the most significant
159 | bits in those cases to get the seed within the proper range again.
160 |
161 |
162 |
Parameters:
163 |
164 |
seed
165 | the random seed to set (integer from 0 - 2^32, negative values will be made positive)
166 |
local socket = require("socket") -- gettime() has higher precision than os.time()
180 | local uuid = require("uuid")
181 | -- see also example at uuid.seed()
182 | uuid.randomseed(socket.gettime()*10000)
183 | print("here's a new uuid: ",uuid())
184 |
185 |
186 |
187 |
188 |
189 | seed ()
190 |
191 |
192 | Seeds the random generator.
193 | It does so in 2 possible ways;
194 |
1. use `os.time()`: this only offers resolution to one second (used when
195 | LuaSocket hasn't been loaded yet
196 | 2. use luasocket `gettime()` function, but it only does so when LuaSocket
197 | has been required already.
198 |
199 |
200 |
201 |
202 |
203 |
204 |
Usage:
205 |
206 |
local socket = require("socket") -- gettime() has higher precision than os.time()
207 | -- LuaSocket loaded, so below line does the same as the example from randomseed()
208 | uuid.seed()
209 | print("here's a new uuid: ",uuid())
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 | generated by LDoc 1.4.6
220 | Last updated 2021-11-05 15:42:41
221 |
222 |
223 |
224 |
225 |
--------------------------------------------------------------------------------
/nakama/util/uuid.lua:
--------------------------------------------------------------------------------
1 | ---------------------------------------------------------------------------------------
2 | -- Work with universally unique identifiers (UUIDs).
3 | --
4 | -- Copyright 2012 Rackspace (original), 2013 Thijs Schreijer (modifications),
5 | -- 2020
6 | --
7 | -- Licensed under the Apache License, Version 2.0 (the "License");
8 | -- you may not use this file except in compliance with the License.
9 | -- You may obtain a copy of the License at
10 | --
11 | -- http://www.apache.org/licenses/LICENSE-2.0
12 | --
13 | -- Unless required by applicable law or agreed to in writing, software
14 | -- distributed under the License is distributed on an "AS-IS" BASIS,
15 | -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | -- See the License for the specific language governing permissions and
17 | -- limitations under the License.
18 | --
19 | -- see http://www.ietf.org/rfc/rfc4122.txt
20 | --
21 | -- Note that this is not a true version 4 (random) UUID. Since `os.time()` precision is only 1 second, it would be hard
22 | -- to guarantee spacial uniqueness when two hosts generate a uuid after being seeded during the same second. This
23 | -- is solved by using the node field from a version 1 UUID. It represents the mac address.
24 | --
25 | -- 28-apr-2013 modified by Thijs Schreijer from the original [Rackspace code](https://github.com/kans/zirgo/blob/807250b1af6725bad4776c931c89a784c1e34db2/util/uuid.lua) as a generic Lua module.
26 | -- Regarding the above mention on `os.time()`; the modifications use the `socket.gettime()` function from LuaSocket
27 | -- if available and hence reduce that problem (provided LuaSocket has been loaded before uuid).
28 | --
29 | -- **6-nov-2015 Please take note of this issue**; [https://github.com/Mashape/kong/issues/478](https://github.com/Mashape/kong/issues/478)
30 | -- It demonstrates the problem of using time as a random seed. Specifically when used from multiple processes.
31 | -- So make sure to seed only once, application wide. And to not have multiple processes do that
32 | -- simultaneously (like nginx does for example).
33 | --
34 | -- 18-jun-2020 modified by [@uncleNight](https://github.com/uncleNight) - dirty workaround for Defold compatibility:
35 | -- removed require() for 'math', 'os' and 'string' modules since Defold Lua runtime exports them globally, so
36 | -- requiring them breaks [bob](https://defold.com/manuals/bob/) builds.
37 | --
38 | -- @module nakama.util.uuid
39 | --
40 |
41 | local M = {}
42 |
43 | local bitsize = 32 -- bitsize assumed for Lua VM. See randomseed function below.
44 | local lua_version = tonumber(_VERSION:match("%d%.*%d*")) -- grab Lua version used
45 |
46 | local MATRIX_AND = {{0,0},{0,1} }
47 | local MATRIX_OR = {{0,1},{1,1}}
48 | local HEXES = '0123456789abcdef'
49 |
50 | local math_floor = math.floor
51 | local math_random = math.random
52 | local math_abs = math.abs
53 | local string_sub = string.sub
54 | local to_number = tonumber
55 | local assert = assert
56 | local type = type
57 |
58 | -- performs the bitwise operation specified by truth matrix on two numbers.
59 | local function BITWISE(x, y, matrix)
60 | local z = 0
61 | local pow = 1
62 | while x > 0 or y > 0 do
63 | z = z + (matrix[x%2+1][y%2+1] * pow)
64 | pow = pow * 2
65 | x = math_floor(x/2)
66 | y = math_floor(y/2)
67 | end
68 | return z
69 | end
70 |
71 | local function INT2HEX(x)
72 | local s,base = '',16
73 | local d
74 | while x > 0 do
75 | d = x % base + 1
76 | x = math_floor(x/base)
77 | s = string_sub(HEXES, d, d)..s
78 | end
79 | while #s < 2 do s = "0" .. s end
80 | return s
81 | end
82 |
83 | ----------------------------------------------------------------------------
84 | -- Creates a new uuid. Either provide a unique hex string, or make sure the
85 | -- random seed is properly set. The module table itself is a shortcut to this
86 | -- function, so `my_uuid = uuid.new()` equals `my_uuid = uuid()`.
87 | --
88 | -- For proper use there are 3 options;
89 | --
90 | -- 1. first require `luasocket`, then call `uuid.seed()`, and request a uuid using no
91 | -- parameter, eg. `my_uuid = uuid()`
92 | -- 2. use `uuid` without `luasocket`, set a random seed using `uuid.randomseed(some_good_seed)`,
93 | -- and request a uuid using no parameter, eg. `my_uuid = uuid()`
94 | -- 3. use `uuid` without `luasocket`, and request a uuid using an unique hex string,
95 | -- eg. `my_uuid = uuid(my_networkcard_macaddress)`
96 | --
97 | -- @return a properly formatted uuid string
98 | -- @param hwaddr (optional) string containing a unique hex value (e.g.: `00:0c:29:69:41:c6`), to be used to compensate for the lesser `math_random()` function. Use a mac address for solid results. If omitted, a fully randomized uuid will be generated, but then you must ensure that the random seed is set properly!
99 | -- @usage
100 | -- local uuid = require("uuid")
101 | -- print("here's a new uuid: ",uuid())
102 | function M.new(hwaddr)
103 | -- bytes are treated as 8bit unsigned bytes.
104 | local bytes = {
105 | math_random(0, 255),
106 | math_random(0, 255),
107 | math_random(0, 255),
108 | math_random(0, 255),
109 | math_random(0, 255),
110 | math_random(0, 255),
111 | math_random(0, 255),
112 | math_random(0, 255),
113 | math_random(0, 255),
114 | math_random(0, 255),
115 | math_random(0, 255),
116 | math_random(0, 255),
117 | math_random(0, 255),
118 | math_random(0, 255),
119 | math_random(0, 255),
120 | math_random(0, 255)
121 | }
122 |
123 | if hwaddr then
124 | assert(type(hwaddr)=="string", "Expected hex string, got "..type(hwaddr))
125 | -- Cleanup provided string, assume mac address, so start from back and cleanup until we've got 12 characters
126 | local i,str = #hwaddr, hwaddr
127 | hwaddr = ""
128 | while i>0 and #hwaddr<12 do
129 | local c = str:sub(i,i):lower()
130 | if HEXES:find(c, 1, true) then
131 | -- valid HEX character, so append it
132 | hwaddr = c..hwaddr
133 | end
134 | i = i - 1
135 | end
136 | assert(#hwaddr == 12, "Provided string did not contain at least 12 hex characters, retrieved '"..hwaddr.."' from '"..str.."'")
137 |
138 | -- no split() in lua. :(
139 | bytes[11] = to_number(hwaddr:sub(1, 2), 16)
140 | bytes[12] = to_number(hwaddr:sub(3, 4), 16)
141 | bytes[13] = to_number(hwaddr:sub(5, 6), 16)
142 | bytes[14] = to_number(hwaddr:sub(7, 8), 16)
143 | bytes[15] = to_number(hwaddr:sub(9, 10), 16)
144 | bytes[16] = to_number(hwaddr:sub(11, 12), 16)
145 | end
146 |
147 | -- set the version
148 | bytes[7] = BITWISE(bytes[7], 0x0f, MATRIX_AND)
149 | bytes[7] = BITWISE(bytes[7], 0x40, MATRIX_OR)
150 | -- set the variant
151 | bytes[9] = BITWISE(bytes[7], 0x3f, MATRIX_AND)
152 | bytes[9] = BITWISE(bytes[7], 0x80, MATRIX_OR)
153 | return INT2HEX(bytes[1])..INT2HEX(bytes[2])..INT2HEX(bytes[3])..INT2HEX(bytes[4]).."-"..
154 | INT2HEX(bytes[5])..INT2HEX(bytes[6]).."-"..
155 | INT2HEX(bytes[7])..INT2HEX(bytes[8]).."-"..
156 | INT2HEX(bytes[9])..INT2HEX(bytes[10]).."-"..
157 | INT2HEX(bytes[11])..INT2HEX(bytes[12])..INT2HEX(bytes[13])..INT2HEX(bytes[14])..INT2HEX(bytes[15])..INT2HEX(bytes[16])
158 | end
159 |
160 | ----------------------------------------------------------------------------
161 | -- Improved randomseed function.
162 | -- Lua 5.1 and 5.2 both truncate the seed given if it exceeds the integer
163 | -- range. If this happens, the seed will be 0 or 1 and all randomness will
164 | -- be gone (each application run will generate the same sequence of random
165 | -- numbers in that case). This improved version drops the most significant
166 | -- bits in those cases to get the seed within the proper range again.
167 | -- @param seed the random seed to set (integer from 0 - 2^32, negative values will be made positive)
168 | -- @return the (potentially modified) seed used
169 | -- @usage
170 | -- local socket = require("socket") -- gettime() has higher precision than os.time()
171 | -- local uuid = require("uuid")
172 | -- -- see also example at uuid.seed()
173 | -- uuid.randomseed(socket.gettime()*10000)
174 | -- print("here's a new uuid: ",uuid())
175 | function M.randomseed(seed)
176 | seed = math_floor(math_abs(seed))
177 | if seed >= (2^bitsize) then
178 | -- integer overflow, so reduce to prevent a bad seed
179 | seed = seed - math_floor(seed / 2^bitsize) * (2^bitsize)
180 | end
181 | if lua_version < 5.2 then
182 | -- 5.1 uses (incorrect) signed int
183 | math.randomseed(seed - 2^(bitsize-1))
184 | else
185 | -- 5.2 uses (correct) unsigned int
186 | math.randomseed(seed)
187 | end
188 | return seed
189 | end
190 |
191 | ----------------------------------------------------------------------------
192 | -- Seeds the random generator.
193 | -- It does so in 2 possible ways;
194 | --
195 | -- 1. use `os.time()`: this only offers resolution to one second (used when
196 | -- LuaSocket hasn't been loaded yet
197 | -- 2. use luasocket `gettime()` function, but it only does so when LuaSocket
198 | -- has been required already.
199 | -- @usage
200 | -- local socket = require("socket") -- gettime() has higher precision than os.time()
201 | -- -- LuaSocket loaded, so below line does the same as the example from randomseed()
202 | -- uuid.seed()
203 | -- print("here's a new uuid: ",uuid())
204 | function M.seed()
205 | if package.loaded["socket"] and package.loaded["socket"].gettime then
206 | return M.randomseed(package.loaded["socket"].gettime()*10000)
207 | else
208 | return M.randomseed(os.time())
209 | end
210 | end
211 |
212 | return setmetatable( M, { __call = function(self, hwaddr) return self.new(hwaddr) end} )
213 |
--------------------------------------------------------------------------------
/codegen/template-common.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | const COMMON_TEMPLATE string = `
4 | local log = require "nakama.util.log"
5 | local retries = require "nakama.util.retries"
6 | local async = require "nakama.util.async"
7 | local api_session = require "nakama.session"
8 | local socket = require "nakama.socket"
9 | local json = require "nakama.util.json"
10 | local uri = require "nakama.util.uri"
11 | local uri_encode = uri.encode
12 |
13 | local M = {}
14 |
15 | -- cancellation tokens associated with a coroutine
16 | local cancellation_tokens = {}
17 |
18 | -- cancel a cancellation token
19 | function M.cancel(token)
20 | assert(token)
21 | token.cancelled = true
22 | end
23 |
24 | -- create a cancellation token
25 | -- use this to cancel an ongoing API call or a sequence of API calls
26 | -- @return token Pass the token to a call to nakama.sync() or to any of the API calls
27 | function M.cancellation_token()
28 | local token = {
29 | cancelled = false
30 | }
31 | function token.cancel()
32 | token.cancelled = true
33 | end
34 | return token
35 | end
36 |
37 | -- Private
38 | -- Run code within a coroutine
39 | -- @param fn The code to run
40 | -- @param cancellation_token Optional cancellation token to cancel the running code
41 | function M.sync(fn, cancellation_token)
42 | assert(fn)
43 | local co = nil
44 | co = coroutine.create(function()
45 | cancellation_tokens[co] = cancellation_token
46 | fn()
47 | cancellation_tokens[co] = nil
48 | end)
49 | local ok, err = coroutine.resume(co)
50 | if not ok then
51 | log(err)
52 | cancellation_tokens[co] = nil
53 | end
54 | end
55 |
56 | -- http request helper used to reduce code duplication in all API functions below
57 | local function http(client, callback, url_path, query_params, method, post_data, retry_policy, cancellation_token, handler_fn)
58 | if callback then
59 | log(url_path, "with callback")
60 | client.engine.http(client.config, url_path, query_params, method, post_data, retry_policy, cancellation_token, function(result)
61 | if not cancellation_token or not cancellation_token.cancelled then
62 | callback(handler_fn(result))
63 | end
64 | end)
65 | else
66 | log(url_path, "with coroutine")
67 | local co = coroutine.running()
68 | assert(co, "You must be running this from withing a coroutine")
69 |
70 | -- get cancellation token associated with this coroutine
71 | cancellation_token = cancellation_tokens[co]
72 | if cancellation_token and cancellation_token.cancelled then
73 | cancellation_tokens[co] = nil
74 | return
75 | end
76 |
77 | return async(function(done)
78 | client.engine.http(client.config, url_path, query_params, method, post_data, retry_policy, cancellation_token, function(result)
79 | if cancellation_token and cancellation_token.cancelled then
80 | cancellation_tokens[co] = nil
81 | return
82 | end
83 | done(handler_fn(result))
84 | end)
85 | end)
86 | end
87 | end
88 |
89 | --
90 | -- Enums
91 | --
92 |
93 | {{- range $defname, $definition := .Definitions }}
94 | {{- $classname := $defname | title }}
95 | {{- if $definition.Enum }}
96 |
97 | --- {{ $classname | pascalToSnake }}
98 | -- {{ $definition.Description | stripNewlines }}
99 | {{- range $i, $enum := $definition.Enum }}
100 | M.{{ $classname | uppercase }}_{{ $enum }} = "{{ $enum }}"
101 | {{- end }}
102 | {{- end }}
103 | {{- end }}
104 |
105 |
106 | --
107 | -- Objects
108 | --
109 |
110 | {{- range $defname, $definition := .Definitions }}
111 | {{- $classname := $defname | title }}
112 | {{- if $definition.Properties }}
113 |
114 | --- create_{{ $classname | pascalToSnake }}
115 | -- {{ $definition.Description | stripNewlines }}
116 | {{- range $propname, $property := $definition.Properties }}
117 | {{- $luaType := luaType $property.Type $property.Ref }}
118 | {{- $varName := varName $propname $property.Type $property.Ref | pascalToSnake }}
119 | -- @param {{ $varName }} ({{ $luaType }}) {{ $property.Description | stripNewlines}}
120 | {{- end }}
121 | function M.create_{{ $classname | pascalToSnake }}(
122 | {{- range $propname, $property := $definition.Properties }}
123 | {{- $luaType := luaType $property.Type $property.Ref }}
124 | {{- $varName := varName $propname $property.Type $property.Ref | pascalToSnake }}{{ $varName }}, {{- end }}_)
125 | {{- range $propname, $property := $definition.Properties }}
126 | {{- $luaType := luaType $property.Type $property.Ref }}
127 | {{- $varName := varName $propname $property.Type $property.Ref | pascalToSnake }}
128 | assert(not {{ $varName }} or type({{ $varName }}) == "{{ $luaType }}", "Argument '{{ $varName }}' must be 'nil' or of type '{{ $luaType }}'")
129 | {{- end }}
130 | return {
131 | {{- range $propname, $property := $definition.Properties }}
132 | {{- $luaType := luaType $property.Type $property.Ref }}
133 | {{- $varName := varName $propname $property.Type $property.Ref | pascalToSnake }}
134 | ["{{ $propname | pascalToSnake }}"] = {{ $varName}},
135 | {{- end }}
136 | }
137 | end
138 | {{- end }}
139 | {{- end }}
140 |
141 |
142 | {{- range $url, $path := .Paths }}
143 | {{- range $method, $operation := $path}}
144 |
145 | --- {{ $operation.OperationId | pascalToSnake | removePrefix }}
146 | -- {{ $operation.Summary | stripNewlines }}
147 | -- @param client Client.
148 | {{- range $i, $parameter := $operation.Parameters }}
149 | {{- $luaType := luaType $parameter.Type $parameter.Schema.Ref }}
150 | {{- $varName := varName $parameter.Name $parameter.Type $parameter.Schema.Ref }}
151 | {{- $varName := $varName | pascalToSnake }}
152 | {{- $varComment := varComment $parameter.Name $parameter.Type $parameter.Schema.Ref $parameter.Items.Type }}
153 | {{- if and (eq $parameter.In "body") $parameter.Schema.Ref }}
154 | {{- bodyFunctionArgsDocs $parameter.Schema.Ref }}
155 | {{- end }}
156 | {{- if and (eq $parameter.In "body") $parameter.Schema.Type }}
157 | -- @param {{ $parameter.Name }} ({{ $parameter.Schema.Type }}) {{ $parameter.Description | stripNewlines }}
158 | {{- end }}
159 | {{- if ne $parameter.In "body" }}
160 | -- @param {{ $varName }} ({{ $parameter.Schema.Type }}) {{ $parameter.Description | stripNewlines }}
161 | {{- end }}
162 |
163 | {{- end }}
164 | -- @param callback (function) Optional callback function
165 | -- A coroutine is used and the result is returned if no callback function is provided.
166 | -- @param retry_policy (function) Optional retry policy used specifically for this call or nil
167 | -- @param cancellation_token (table) Optional cancellation token for this call
168 | -- @return The result.
169 | function M.{{ $operation.OperationId | pascalToSnake | removePrefix }}(client
170 | {{- range $i, $parameter := $operation.Parameters }}
171 | {{- $luaType := luaType $parameter.Type $parameter.Schema.Ref }}
172 | {{- $varName := varName $parameter.Name $parameter.Type $parameter.Schema.Ref }}
173 | {{- $varName := $varName | pascalToSnake }}
174 | {{- $varComment := varComment $parameter.Name $parameter.Type $parameter.Schema.Ref $parameter.Items.Type }}
175 | {{- if and (eq $parameter.In "body") $parameter.Schema.Ref }}
176 | {{- bodyFunctionArgs $parameter.Schema.Ref}}
177 | {{- end }}
178 | {{- if and (eq $parameter.In "body") $parameter.Schema.Type }}, {{ $parameter.Name }} {{- end }}
179 | {{- if ne $parameter.In "body" }}, {{ $varName }} {{- end }}
180 | {{- end }}, callback, retry_policy, cancellation_token)
181 | assert(client, "You must provide a client")
182 | {{- range $parameter := $operation.Parameters }}
183 | {{- $varName := varName $parameter.Name $parameter.Type $parameter.Schema.Ref }}
184 | {{- if eq $parameter.In "body" }}
185 | {{- bodyFunctionArgsAssert $parameter.Schema.Ref}}
186 | {{- end }}
187 | {{- if and (eq $parameter.In "body") $parameter.Schema.Type }}
188 | assert({{- if $parameter.Required }}{{ $parameter.Name }} and {{ end }}type({{ $parameter.Name }}) == "{{ $parameter.Schema.Type }}", "Argument '{{ $parameter.Name }}' must be of type '{{ $parameter.Schema.Type }}'")
189 | {{- end }}
190 |
191 | {{- end }}
192 |
193 | {{- if $operation.OperationId | isAuthenticateMethod }}
194 | -- unset the token so username+password credentials will be used
195 | client.config.bearer_token = nil
196 |
197 | {{- end}}
198 |
199 | local url_path = "{{- $url }}"
200 | {{- range $parameter := $operation.Parameters }}
201 | {{- $varName := varName $parameter.Name $parameter.Type $parameter.Schema.Ref }}
202 | {{- if eq $parameter.In "path" }}
203 | url_path = url_path:gsub("{{- print "{" $parameter.Name "}"}}", uri_encode({{ $varName | pascalToSnake }}))
204 | {{- end }}
205 | {{- end }}
206 |
207 | local query_params = {}
208 | {{- range $parameter := $operation.Parameters}}
209 | {{- $varName := varName $parameter.Name $parameter.Type $parameter.Schema.Ref }}
210 | {{- if eq $parameter.In "query"}}
211 | query_params["{{- $parameter.Name }}"] = {{ $varName | pascalToSnake }}
212 | {{- end}}
213 | {{- end}}
214 |
215 | local post_data = nil
216 | {{- range $parameter := $operation.Parameters }}
217 | {{- $varName := varName $parameter.Name $parameter.Type $parameter.Schema.Ref }}
218 | {{- if eq $parameter.In "body" }}
219 | {{- if $parameter.Schema.Ref }}
220 | post_data = json.encode({
221 | {{- bodyFunctionArgsTable $parameter.Schema.Ref}} })
222 | {{- end }}
223 | {{- if $parameter.Schema.Type }}
224 | post_data = json.encode({{ $parameter.Name }})
225 | {{- end }}
226 | {{- end }}
227 | {{- end }}
228 |
229 | return http(client, callback, url_path, query_params, "{{- $method | uppercase }}", post_data, retry_policy, cancellation_token, function(result)
230 | {{- if $operation.Responses.Ok.Schema.Ref }}
231 | if not result.error and {{ $operation.Responses.Ok.Schema.Ref | cleanRef | pascalToSnake }} then
232 | result = {{ $operation.Responses.Ok.Schema.Ref | cleanRef | pascalToSnake }}.create(result)
233 | end
234 | {{- end }}
235 | return result
236 | end)
237 | end
238 | {{- end }}
239 | {{- end }}
240 | `
--------------------------------------------------------------------------------
/codegen/generate-rest.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 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 | package main
16 |
17 | import (
18 | "bufio"
19 | "encoding/json"
20 | "flag"
21 | "fmt"
22 | "io/ioutil"
23 | "os"
24 | "strings"
25 | "text/template"
26 | "sort"
27 | )
28 |
29 |
30 | var schema struct {
31 | Paths map[string]map[string]struct {
32 | Summary string
33 | OperationId string
34 | Responses struct {
35 | Ok struct {
36 | Schema struct {
37 | Ref string `json:"$ref"`
38 | }
39 | } `json:"200"`
40 | }
41 | Parameters []struct {
42 | Name string
43 | Description string
44 | In string
45 | Required bool
46 | Type string // used with primitives
47 | Items struct { // used with type "array"
48 | Type string
49 | }
50 | Schema struct { // used with http body
51 | Type string
52 | Ref string `json:"$ref"`
53 | }
54 | Format string // used with type "boolean"
55 | }
56 | Security []map[string][]struct {
57 | }
58 | }
59 | Definitions map[string]struct {
60 | Properties map[string]struct {
61 | Type string
62 | Ref string `json:"$ref"` // used with object
63 | Items struct { // used with type "array"
64 | Type string
65 | Ref string `json:"$ref"`
66 | }
67 | AdditionalProperties struct {
68 | Type string // used with type "map"
69 | }
70 | Format string // used with type "boolean"
71 | Description string
72 | }
73 | Enum []string
74 | Description string
75 | // used only by enums
76 | Title string
77 | }
78 | }
79 |
80 | func convertRefToClassName(input string) (className string) {
81 | cleanRef := strings.TrimPrefix(input, "#/definitions/")
82 | className = strings.Title(cleanRef)
83 | return
84 | }
85 |
86 | func stripNewlines(input string) (output string) {
87 | output = strings.Replace(input, "\n", "\n--", -1)
88 | return
89 | }
90 |
91 | func pascalToSnake(input string) (output string) {
92 | output = ""
93 | prev_low := false
94 | for _, v := range input {
95 | is_cap := v >= 'A' && v <= 'Z'
96 | is_low := v >= 'a' && v <= 'z'
97 | if is_cap && prev_low {
98 | output = output + "_"
99 | }
100 | output += strings.ToLower(string(v))
101 | prev_low = is_low
102 | }
103 | return
104 | }
105 |
106 | // camelToPascal converts a string from camel case to Pascal case.
107 | func camelToPascal(camelCase string) (pascalCase string) {
108 | if len(camelCase) <= 0 {
109 | return ""
110 | }
111 | pascalCase = strings.ToUpper(string(camelCase[0])) + camelCase[1:]
112 | return
113 | }
114 | // pascalToCamel converts a Pascal case string to a camel case string.
115 | func pascalToCamel(input string) (camelCase string) {
116 | if input == "" {
117 | return ""
118 | }
119 | camelCase = strings.ToLower(string(input[0]))
120 | camelCase += string(input[1:])
121 | return camelCase
122 | }
123 |
124 | func removePrefix(input string) (output string) {
125 | output = strings.Replace(input, "nakama_", "", -1)
126 | output = strings.Replace(output, "satori_", "", -1)
127 | return
128 | }
129 |
130 | func isEnum(ref string) bool {
131 | // swagger schema definition keys have inconsistent casing
132 | var camelOk bool
133 | var pascalOk bool
134 | var enums []string
135 |
136 | cleanedRef := convertRefToClassName(ref)
137 | asCamel := pascalToCamel(cleanedRef)
138 | if _, camelOk = schema.Definitions[asCamel]; camelOk {
139 | enums = schema.Definitions[asCamel].Enum
140 | }
141 |
142 | asPascal := camelToPascal(cleanedRef)
143 | if _, pascalOk = schema.Definitions[asPascal]; pascalOk {
144 | enums = schema.Definitions[asPascal].Enum
145 | }
146 |
147 | if !pascalOk && !camelOk {
148 | return false
149 | }
150 |
151 | return len(enums) > 0
152 | }
153 |
154 | // Parameter type to Lua type
155 | func luaType(p_type string, p_ref string) (out string) {
156 | if isEnum(p_ref) {
157 | out = "string"
158 | return
159 | }
160 | switch p_type {
161 | case "integer": out = "number"
162 | case "string": out = "string"
163 | case "boolean": out = "boolean"
164 | case "array": out = "table"
165 | case "object": out = "table"
166 | default: out = "table"
167 | }
168 | return
169 | }
170 |
171 | // Default value for Lua types
172 | func luaDef(p_type string, p_ref string) (out string) {
173 | switch(p_type) {
174 | case "integer": out = "0"
175 | case "string": out = "\"\""
176 | case "boolean": out = "false"
177 | case "array": out = "{}"
178 | case "object": out = "{ _ = '' }"
179 | default: out = "M.create_" + pascalToSnake(convertRefToClassName(p_ref)) + "()"
180 | }
181 | return
182 | }
183 |
184 | // Lua variable name from name, type and ref
185 | func varName(p_name string, p_type string, p_ref string) (out string) {
186 | p_name = strings.Replace(p_name, "@", "", -1)
187 | switch(p_type) {
188 | case "integer": out = p_name + "_int"
189 | case "string": out = p_name + "_str"
190 | case "boolean": out = p_name + "_bool"
191 | case "array": out = p_name + "_arr"
192 | case "object": out = p_name + "_obj"
193 | default: out = p_name + "_" + pascalToSnake(convertRefToClassName(p_ref))
194 | }
195 | return
196 | }
197 |
198 | func varComment(p_name string, p_type string, p_ref string, p_item_type string) (out string) {
199 | switch(p_type) {
200 | case "integer": out = "number"
201 | case "string": out = "string"
202 | case "boolean": out = "boolean"
203 | case "array": out = "table (" + luaType(p_item_type, p_ref) + ")"
204 | case "object": out = "table (object)"
205 | default: out = "table (" + pascalToSnake(convertRefToClassName(p_ref)) + ")"
206 | }
207 | return
208 | }
209 |
210 | func isAuthenticateMethod(input string) (output bool) {
211 | output = strings.HasPrefix(input, "Nakama_Authenticate")
212 | return
213 | }
214 |
215 | func main() {
216 | // Argument flags
217 | var output = flag.String("output", "", "The output for generated code.")
218 | flag.Parse()
219 |
220 | inputs := flag.Args()
221 | if len(inputs) < 1 {
222 | fmt.Printf("No input file found: %s\n\n", inputs)
223 | fmt.Println("openapi-gen [flags] inputs...")
224 | flag.PrintDefaults()
225 | return
226 | }
227 |
228 | input := inputs[0]
229 | content, err := ioutil.ReadFile(input)
230 | if err != nil {
231 | fmt.Printf("Unable to read file: %s\n", err)
232 | return
233 | }
234 |
235 |
236 | if err := json.Unmarshal(content, &schema); err != nil {
237 | fmt.Printf("Unable to decode input %s : %s\n", input, err)
238 | return
239 | }
240 |
241 |
242 | // expand the body argument to individual function arguments
243 | bodyFunctionArgs := func(ref string) (output string) {
244 | ref = strings.Replace(ref, "#/definitions/", "", -1)
245 | props := schema.Definitions[ref].Properties
246 | keys := make([]string, 0, len(props))
247 | for prop := range props {
248 | keys = append(keys, prop)
249 | }
250 | sort.Strings(keys)
251 | for _,key := range keys {
252 | output = output + ", " + key
253 | }
254 | return
255 | }
256 |
257 | // expand the body argument to individual function argument docs
258 | bodyFunctionArgsDocs := func(ref string) (output string) {
259 | ref = strings.Replace(ref, "#/definitions/", "", -1)
260 | output = "\n"
261 | props := schema.Definitions[ref].Properties
262 | keys := make([]string, 0, len(props))
263 | for prop := range props {
264 | keys = append(keys, prop)
265 | }
266 | sort.Strings(keys)
267 | for _,key := range keys {
268 | info := props[key]
269 | output = output + "-- @param " + key + " (" + info.Type + ") " + stripNewlines(info.Description) + "\n"
270 | }
271 | return
272 | }
273 |
274 | // expand the body argument to individual asserts for the call args
275 | bodyFunctionArgsAssert := func(ref string) (output string) {
276 | ref = strings.Replace(ref, "#/definitions/", "", -1)
277 | output = "\n"
278 | props := schema.Definitions[ref].Properties
279 | keys := make([]string, 0, len(props))
280 | for prop := range props {
281 | keys = append(keys, prop)
282 | }
283 | sort.Strings(keys)
284 | for _,key := range keys {
285 | info := props[key]
286 | luaType := luaType(info.Type, info.Ref)
287 | output = output + "\tassert(not " + key + " or type(" + key + ") == \"" + luaType + "\", \"Argument '" + key + "' must be 'nil' or of type '" + luaType + "'\")\n"
288 | }
289 | return
290 | }
291 |
292 | // expand the body argument to individual asserts for the message body table
293 | bodyFunctionArgsTable := func(ref string) (output string) {
294 | ref = strings.Replace(ref, "#/definitions/", "", -1)
295 | output = "\n"
296 | props := schema.Definitions[ref].Properties
297 | keys := make([]string, 0, len(props))
298 | for prop := range props {
299 | keys = append(keys, prop)
300 | }
301 | sort.Strings(keys)
302 | for _,key := range keys {
303 | output = output + "\t" + key + " = " + key + ",\n"
304 | }
305 | return
306 | }
307 |
308 |
309 | fmap := template.FuncMap {
310 | "cleanRef": convertRefToClassName,
311 | "stripNewlines": stripNewlines,
312 | "title": strings.Title,
313 | "uppercase": strings.ToUpper,
314 | "pascalToSnake": pascalToSnake,
315 | "luaType": luaType,
316 | "luaDef": luaDef,
317 | "varName": varName,
318 | "varComment": varComment,
319 | "bodyFunctionArgsDocs": bodyFunctionArgsDocs,
320 | "bodyFunctionArgs": bodyFunctionArgs,
321 | "bodyFunctionArgsAssert": bodyFunctionArgsAssert,
322 | "bodyFunctionArgsTable": bodyFunctionArgsTable,
323 | "isEnum": isEnum,
324 | "isAuthenticateMethod": isAuthenticateMethod,
325 | "removePrefix": removePrefix,
326 | }
327 |
328 | MAIN_TEMPLATE := strings.Replace(MAIN_TEMPLATE, "%%COMMON_TEMPLATE%%", COMMON_TEMPLATE, 1)
329 | tmpl, err := template.New(input).Funcs(fmap).Parse(MAIN_TEMPLATE)
330 | if err != nil {
331 | fmt.Printf("Template parse error: %s\n", err)
332 | return
333 | }
334 |
335 | if len(*output) < 1 {
336 | tmpl.Execute(os.Stdout, schema)
337 | return
338 | }
339 |
340 | f, err := os.Create(*output)
341 | if err != nil {
342 | fmt.Printf("Unable to create file: %s\n", err)
343 | return
344 | }
345 | defer f.Close()
346 |
347 | writer := bufio.NewWriter(f)
348 | tmpl.Execute(writer, schema)
349 | writer.Flush()
350 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/codegen/generate-nakama-realtime.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import re
4 | import sys
5 | import os
6 |
7 | SOCKET_LUA = """
8 | local M = {}
9 |
10 | local b64 = require "nakama.util.b64"
11 | local async = require "nakama.util.async"
12 | local log = require "nakama.util.log"
13 |
14 | local function on_socket_message(socket, message)
15 | if message.match_data then
16 | message.match_data.data = b64.decode(message.match_data.data)
17 | end
18 | if message.cid then
19 | local callback = socket.requests[message.cid]
20 | if callback then
21 | callback(message)
22 | end
23 | socket.requests[message.cid] = nil
24 | end
25 | for event_id,_ in pairs(message) do
26 | if socket.events[event_id] then
27 | socket.events[event_id](message)
28 | return
29 | end
30 | end
31 | log("Unhandled message")
32 | end
33 |
34 | local function socket_send(socket, message, callback)
35 | if message.match_data_send and message.match_data_send.data then
36 | message.match_data_send.data = b64.encode(message.match_data_send.data)
37 | end
38 | if callback then
39 | if message.cid then
40 | socket.requests[message.cid] = callback
41 | socket.engine.socket_send(socket, message)
42 | else
43 | socket.engine.socket_send(socket, message)
44 | callback({})
45 | end
46 | else
47 | return async(function(done)
48 | if message.cid then
49 | socket.requests[message.cid] = done
50 | socket.engine.socket_send(socket, message)
51 | else
52 | socket.engine.socket_send(socket, message)
53 | done({})
54 | end
55 | end)
56 | end
57 | end
58 |
59 |
60 | function M.create(client)
61 | local socket = client.engine.socket_create(client.config, on_socket_message)
62 | assert(socket, "No socket created")
63 | assert(type(socket) == "table", "The created instance must be a table")
64 | socket.client = client
65 | socket.engine = client.engine
66 |
67 | -- callbacks
68 | socket.cid = 0
69 | socket.requests = {}
70 |
71 | -- event handlers are registered here
72 | socket.events = {}
73 |
74 | -- set up function mappings on the socket instance itself
75 | for name,fn in pairs(M) do
76 | if name ~= "create" and type(fn) == "function" then
77 | socket[name] = function(...) return fn(socket, ...) end
78 | end
79 | end
80 | return socket
81 | end
82 |
83 |
84 | --- Attempt to connect a Nakama socket to the server.
85 | -- @param socket The client socket to connect (from call to create_socket).
86 | -- @param callback Optional callback to invoke with the result.
87 | -- @return If no callback is provided the function returns the result.
88 | function M.connect(socket, callback)
89 | assert(socket, "You must provide a socket")
90 | if callback then
91 | socket.engine.socket_connect(socket, callback)
92 | else
93 | return async(function(done)
94 | socket.engine.socket_connect(socket, done)
95 | end)
96 | end
97 | end
98 |
99 |
100 | --- Send message on Nakama socket.
101 | -- @param socket The client socket to use when sending the message.
102 | -- @param message The message string.
103 | -- @param callback Optional callback to invoke with the result.
104 | -- @return If no callback is provided the function returns the result.
105 | function M.send(socket, message, callback)
106 | assert(socket, "You must provide a socket")
107 | assert(message, "You must provide a message")
108 | return socket_send(socket, message, callback)
109 | end
110 |
111 |
112 | --- On disconnect hook.
113 | -- @param socket Nakama Client Socket.
114 | -- @param fn The callback function.
115 | function M.on_disconnect(socket, fn)
116 | assert(socket, "You must provide a socket")
117 | socket.on_disconnect = fn
118 | end
119 |
120 |
121 | --
122 | -- messages
123 | --
124 | -- %s
125 |
126 |
127 | --
128 | -- events
129 | --
130 | -- %s
131 | %s
132 |
133 | -- Default case. Assumed as ROOM type.
134 | M.CHANNELTYPE_UNSPECIFIED = 0
135 | -- A room which anyone can join to chat.
136 | M.CHANNELTYPE_ROOM = 1
137 | -- A private channel for 1-on-1 chat.
138 | M.CHANNELTYPE_DIRECT_MESSAGE = 2
139 | -- A channel for group chat.
140 | M.CHANNELTYPE_GROUP = 3
141 |
142 |
143 | -- An unexpected result from the server.
144 | M.ERROR_RUNTIME_EXCEPTION = 0
145 | -- The server received a message which is not recognised.
146 | M.ERROR_UNRECOGNIZED_PAYLOAD = 1
147 | -- A message was expected but contains no content.
148 | M.ERROR_MISSING_PAYLOAD = 2
149 | -- Fields in the message have an invalid format.
150 | M.ERROR_BAD_INPUT = 3
151 | -- The match id was not found.
152 | M.ERROR_MATCH_NOT_FOUND = 4
153 | -- The match join was rejected.
154 | M.ERROR_MATCH_JOIN_REJECTED = 5
155 | -- The runtime function does not exist on the server.
156 | M.ERROR_RUNTIME_FUNCTION_NOT_FOUND = 6
157 | -- The runtime function executed with an error.
158 | M.ERROR_RUNTIME_FUNCTION_EXCEPTION = 7
159 |
160 | return M
161 | """
162 |
163 | CAMEL_TO_SNAKE = re.compile(r'(?", "map", message)
199 | # remove inner enum
200 | message = re.sub("enum .* \{.*?}?", "", message, 0, re.DOTALL | re.MULTILINE)
201 | # remove inner message
202 | message = re.sub("message .* \{.*?}?", "", message, 0, re.DOTALL | re.MULTILINE)
203 |
204 | properties = []
205 | s = "\s*(repeated )?(\S*) (.*) = .*;"
206 | match = re.findall(s, message)
207 | for m in match:
208 | if m[1]:
209 | lua_type = type_to_lua(m[1])
210 | name = m[2]
211 | repeated = m[0] == "repeated "
212 | if repeated:
213 | lua_type == "table"
214 | properties.append({ "type": lua_type, "name": name, "repeated": repeated})
215 | return properties
216 |
217 |
218 | def message_to_lua(message_id, api, wait_for_callback):
219 | message = get_proto_message(message_id, api)
220 | if not message:
221 | print("Unable to find message %s" % message_id)
222 | return
223 |
224 | props = parse_proto_message(message)
225 | function_args = [ "socket" ]
226 | for prop in props:
227 | function_args.append(prop["name"])
228 | function_args.append("callback")
229 |
230 | function_args_string = ", ".join(function_args)
231 |
232 | message_id = camel_to_snake(message_id)
233 | function_name = message_id
234 |
235 | lua = "\n"
236 | lua = lua + "--- " + function_name + "\n"
237 | for function_arg in function_args:
238 | lua = lua + "-- @param %s\n" % (function_arg)
239 | lua = lua + "function M.%s(%s)\n" % (function_name, function_args_string)
240 | lua = lua + " assert(socket)\n"
241 | for prop in props:
242 | lua = lua + " assert(%s == nil or _G.type(%s) == '%s')\n" % (prop["name"], prop["name"], prop["type"])
243 | if wait_for_callback:
244 | lua = lua + " socket.cid = socket.cid + 1\n"
245 | lua = lua + " local message = {\n"
246 | if wait_for_callback:
247 | lua = lua + " cid = tostring(socket.cid),\n"
248 | lua = lua + " %s = {\n" % message_id
249 | for prop in props:
250 | lua = lua + " %s = %s,\n" % (prop["name"], prop["name"])
251 | lua = lua + " }\n"
252 | lua = lua + " }\n"
253 | lua = lua + " return socket_send(socket, message, callback)\n"
254 | lua = lua + "end\n"
255 | return lua
256 |
257 |
258 | def event_to_lua(event_id, api):
259 | event = get_proto_message(event_id, api)
260 | if not event:
261 | print("Unable to find event %s" % event_id)
262 | return
263 |
264 | event_id = camel_to_snake(event_id)
265 | function_name = "on_" + event_id
266 |
267 | lua = "\n"
268 | lua = lua + "--- " + function_name + "\n"
269 | lua = lua + "-- @param socket Nakama Client Socket.\n"
270 | lua = lua + "-- @param fn The callback function.\n"
271 | lua = lua + "function M.%s(socket, fn)\n" % (function_name)
272 | lua = lua + " assert(socket, \"You must provide a socket\")\n"
273 | lua = lua + " assert(fn, \"You must provide a function\")\n"
274 | lua = lua + " socket.events.%s = fn\n" % (event_id)
275 | lua = lua + "end\n"
276 | return { "name": function_name, "lua": lua }
277 |
278 |
279 |
280 | def messages_to_lua(rtapi):
281 | # list of message names that should generate Lua code
282 | CHANNEL_MESSAGES = [ "ChannelJoin", "ChannelLeave", "ChannelMessageSend", "ChannelMessageRemove", "ChannelMessageUpdate" ]
283 | MATCH_MESSAGES = [ "MatchDataSend", "MatchCreate", "MatchJoin", "MatchLeave" ]
284 | MATCHMAKER_MESSAGES = [ "MatchmakerAdd", "MatchmakerRemove" ]
285 | PARTY_MESSAGES = [ "PartyCreate", "PartyJoin", "PartyLeave", "PartyPromote", "PartyAccept", "PartyRemove", "PartyClose", "PartyJoinRequestList", "PartyMatchmakerAdd", "PartyMatchmakerRemove", "PartyDataSend" ]
286 | STATUS_MESSAGES = [ "StatusFollow", "StatusUnfollow", "StatusUpdate" ]
287 | ALL_MESSAGES = CHANNEL_MESSAGES + MATCH_MESSAGES + MATCHMAKER_MESSAGES + PARTY_MESSAGES + STATUS_MESSAGES
288 |
289 | # list of messages that do not expect a server response
290 | CHANNEL_MESSAGES_NOCB = [ "ChannelLeave" ]
291 | MATCH_MESSAGES_NOCB = [ "MatchLeave", "MatchDataSend"]
292 | MATCHMAKER_MESSAGES_NOCB = [ "MatchmakerRemove" ]
293 | PARTY_MESSAGES_NOCB = [ "PartyDataSend", "PartyAccept", "PartyClose", "PartyJoin", "PartyLeave", "PartyPromote", "PartyRemove", "PartyMatchmakerRemove" ]
294 | STATUS_MESSAGES_NOCB = [ "StatusUnfollow", "StatusUpdate" ]
295 |
296 | NO_CALLBACK_MESSAGES = CHANNEL_MESSAGES_NOCB + MATCH_MESSAGES_NOCB + MATCHMAKER_MESSAGES_NOCB + PARTY_MESSAGES_NOCB + STATUS_MESSAGES_NOCB
297 |
298 | ids = []
299 | lua = ""
300 | for message_id in ALL_MESSAGES:
301 | wait_for_callback = (message_id not in NO_CALLBACK_MESSAGES)
302 | lua = lua + message_to_lua(message_id, rtapi, wait_for_callback)
303 | ids.append(message_id)
304 |
305 | return { "ids": ids, "lua": lua }
306 |
307 |
308 |
309 | def events_to_lua(rtapi, api):
310 | CHANNEL_EVENTS = [ "ChannelPresenceEvent" ]
311 | MATCH_EVENTS = [ "MatchPresenceEvent", "MatchData", "Match" ]
312 | MATCHMAKER_EVENTS = [ "MatchmakerMatched" ]
313 | NOTFICATION_EVENTS = [ "Notifications" ]
314 | PARTY_EVENTS = [ "PartyPresenceEvent", "Party", "PartyData", "PartyJoinRequest", "PartyLeader" ]
315 | STATUS_EVENTS = [ "StatusPresenceEvent", "Status" ]
316 | STREAM_EVENTS = [ "StreamData" ]
317 | OTHER_EVENTS = [ "Error" ]
318 | ALL_EVENTS = CHANNEL_EVENTS + MATCH_EVENTS + MATCHMAKER_EVENTS + NOTFICATION_EVENTS + PARTY_EVENTS + STATUS_EVENTS + STREAM_EVENTS + OTHER_EVENTS
319 |
320 | ids = []
321 | lua = ""
322 | for event_id in ALL_EVENTS:
323 | data = event_to_lua(event_id, rtapi)
324 | ids.append(data["name"])
325 | lua = lua + data["lua"]
326 |
327 | # also add single ChannelMessage event from rest API (it is referenced from the realtime API)
328 | data = event_to_lua("ChannelMessage", api)
329 | ids.append(data["name"])
330 | lua = lua + data["lua"]
331 |
332 | return { "ids": ids, "lua": lua }
333 |
334 |
335 |
336 | if len(sys.argv) < 2:
337 | print("You must provide paths to realtime.proto and api.proto")
338 | sys.exit(1)
339 |
340 | rtapi_path = sys.argv[1]
341 | api_path = sys.argv[2]
342 | out_path = None
343 |
344 | if len(sys.argv) > 3:
345 | out_path = sys.argv[3]
346 |
347 | rtapi = read_as_string(rtapi_path)
348 | api = read_as_string(api_path)
349 |
350 | messages = messages_to_lua(rtapi)
351 | events = events_to_lua(rtapi, api)
352 |
353 | generated_lua = SOCKET_LUA % (messages["lua"], "\n-- ".join(events["ids"]), events["lua"])
354 |
355 | if out_path:
356 | with open(out_path, "w") as f:
357 | f.write(generated_lua)
358 | else:
359 | print(generated_lua)
360 |
361 |
362 |
--------------------------------------------------------------------------------
/nakama/util/json.lua:
--------------------------------------------------------------------------------
1 | ---
2 | -- JSON encode and decode data.
3 | --
4 | -- Copyright (c) 2019 rxi
5 | --
6 | -- Permission is hereby granted, free of charge, to any person obtaining a copy of
7 | -- this software and associated documentation files (the "Software"), to deal in
8 | -- the Software without restriction, including without limitation the rights to
9 | -- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 | -- of the Software, and to permit persons to whom the Software is furnished to do
11 | -- so, subject to the following conditions:
12 | --
13 | -- The above copyright notice and this permission notice shall be included in all
14 | -- copies or substantial portions of the Software.
15 | --
16 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | -- SOFTWARE.
23 | --
24 | -- @module nakama.util.json
25 | --
26 |
27 | local json = { _version = "0.1.2" }
28 |
29 | -------------------------------------------------------------------------------
30 | -- Encode
31 | -------------------------------------------------------------------------------
32 |
33 | local encode
34 |
35 | local escape_char_map = {
36 | [ "\\" ] = "\\\\",
37 | [ "\"" ] = "\\\"",
38 | [ "\b" ] = "\\b",
39 | [ "\f" ] = "\\f",
40 | [ "\n" ] = "\\n",
41 | [ "\r" ] = "\\r",
42 | [ "\t" ] = "\\t",
43 | }
44 |
45 | local escape_char_map_inv = { [ "\\/" ] = "/" }
46 | for k, v in pairs(escape_char_map) do
47 | escape_char_map_inv[v] = k
48 | end
49 |
50 |
51 | local function escape_char(c)
52 | return escape_char_map[c] or string.format("\\u%04x", c:byte())
53 | end
54 |
55 |
56 | local function encode_nil(val)
57 | return "null"
58 | end
59 |
60 |
61 | local function encode_table(val, stack)
62 | local res = {}
63 | stack = stack or {}
64 |
65 | -- Circular reference?
66 | if stack[val] then error("circular reference") end
67 |
68 | stack[val] = true
69 |
70 | if rawget(val, 1) ~= nil or next(val) == nil then
71 | -- Treat as array -- check keys are valid and it is not sparse
72 | local n = 0
73 | for k in pairs(val) do
74 | if type(k) ~= "number" then
75 | error("invalid table: mixed or invalid key types")
76 | end
77 | n = n + 1
78 | end
79 | if n ~= #val then
80 | error("invalid table: sparse array")
81 | end
82 | -- Encode
83 | for i, v in ipairs(val) do
84 | table.insert(res, encode(v, stack))
85 | end
86 | stack[val] = nil
87 | return "[" .. table.concat(res, ",") .. "]"
88 |
89 | else
90 | -- Treat as an object
91 | for k, v in pairs(val) do
92 | if type(k) ~= "string" then
93 | error("invalid table: mixed or invalid key types")
94 | end
95 | table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
96 | end
97 | stack[val] = nil
98 | return "{" .. table.concat(res, ",") .. "}"
99 | end
100 | end
101 |
102 |
103 | local function encode_string(val)
104 | return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
105 | end
106 |
107 |
108 | local function encode_number(val)
109 | -- Check for NaN, -inf and inf
110 | if val ~= val or val <= -math.huge or val >= math.huge then
111 | error("unexpected number value '" .. tostring(val) .. "'")
112 | end
113 | return string.format("%.14g", val)
114 | end
115 |
116 |
117 | local type_func_map = {
118 | [ "nil" ] = encode_nil,
119 | [ "table" ] = encode_table,
120 | [ "string" ] = encode_string,
121 | [ "number" ] = encode_number,
122 | [ "boolean" ] = tostring,
123 | }
124 |
125 |
126 | encode = function(val, stack)
127 | local t = type(val)
128 | local f = type_func_map[t]
129 | if f then
130 | return f(val, stack)
131 | end
132 | error("unexpected type '" .. t .. "'")
133 | end
134 |
135 |
136 | --- JSON encode data.
137 | -- @param val The Lua data to encode.
138 | -- @return The JSON encoded result string.
139 | function json.encode(val)
140 | return ( encode(val) )
141 | end
142 |
143 |
144 | -------------------------------------------------------------------------------
145 | -- Decode
146 | -------------------------------------------------------------------------------
147 |
148 | local parse
149 |
150 | local function create_set(...)
151 | local res = {}
152 | for i = 1, select("#", ...) do
153 | res[ select(i, ...) ] = true
154 | end
155 | return res
156 | end
157 |
158 | local space_chars = create_set(" ", "\t", "\r", "\n")
159 | local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
160 | local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
161 | local literals = create_set("true", "false", "null")
162 |
163 | local literal_map = {
164 | [ "true" ] = true,
165 | [ "false" ] = false,
166 | [ "null" ] = nil,
167 | }
168 |
169 |
170 | local function next_char(str, idx, set, negate)
171 | for i = idx, #str do
172 | if set[str:sub(i, i)] ~= negate then
173 | return i
174 | end
175 | end
176 | return #str + 1
177 | end
178 |
179 |
180 | local function decode_error(str, idx, msg)
181 | local line_count = 1
182 | local col_count = 1
183 | for i = 1, idx - 1 do
184 | col_count = col_count + 1
185 | if str:sub(i, i) == "\n" then
186 | line_count = line_count + 1
187 | col_count = 1
188 | end
189 | end
190 | error( string.format("%s at line %d col %d", msg, line_count, col_count) )
191 | end
192 |
193 |
194 | local function codepoint_to_utf8(n)
195 | -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
196 | local f = math.floor
197 | if n <= 0x7f then
198 | return string.char(n)
199 | elseif n <= 0x7ff then
200 | return string.char(f(n / 64) + 192, n % 64 + 128)
201 | elseif n <= 0xffff then
202 | return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
203 | elseif n <= 0x10ffff then
204 | return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
205 | f(n % 4096 / 64) + 128, n % 64 + 128)
206 | end
207 | error( string.format("invalid unicode codepoint '%x'", n) )
208 | end
209 |
210 |
211 | local function parse_unicode_escape(s)
212 | local n1 = tonumber( s:sub(3, 6), 16 )
213 | local n2 = tonumber( s:sub(9, 12), 16 )
214 | -- Surrogate pair?
215 | if n2 then
216 | return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
217 | else
218 | return codepoint_to_utf8(n1)
219 | end
220 | end
221 |
222 |
223 | local function parse_string(str, i)
224 | local has_unicode_escape = false
225 | local has_surrogate_escape = false
226 | local has_escape = false
227 | local last
228 | for j = i + 1, #str do
229 | local x = str:byte(j)
230 |
231 | if x < 32 then
232 | decode_error(str, j, "control character in string")
233 | end
234 |
235 | if last == 92 then -- "\\" (escape char)
236 | if x == 117 then -- "u" (unicode escape sequence)
237 | local hex = str:sub(j + 1, j + 5)
238 | if not hex:find("%x%x%x%x") then
239 | decode_error(str, j, "invalid unicode escape in string")
240 | end
241 | if hex:find("^[dD][89aAbB]") then
242 | has_surrogate_escape = true
243 | else
244 | has_unicode_escape = true
245 | end
246 | else
247 | local c = string.char(x)
248 | if not escape_chars[c] then
249 | decode_error(str, j, "invalid escape char '" .. c .. "' in string")
250 | end
251 | has_escape = true
252 | end
253 | last = nil
254 |
255 | elseif x == 34 then -- '"' (end of string)
256 | local s = str:sub(i + 1, j - 1)
257 | if has_surrogate_escape then
258 | s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
259 | end
260 | if has_unicode_escape then
261 | s = s:gsub("\\u....", parse_unicode_escape)
262 | end
263 | if has_escape then
264 | s = s:gsub("\\.", escape_char_map_inv)
265 | end
266 | return s, j + 1
267 |
268 | else
269 | last = x
270 | end
271 | end
272 | decode_error(str, i, "expected closing quote for string")
273 | end
274 |
275 |
276 | local function parse_number(str, i)
277 | local x = next_char(str, i, delim_chars)
278 | local s = str:sub(i, x - 1)
279 | local n = tonumber(s)
280 | if not n then
281 | decode_error(str, i, "invalid number '" .. s .. "'")
282 | end
283 | return n, x
284 | end
285 |
286 |
287 | local function parse_literal(str, i)
288 | local x = next_char(str, i, delim_chars)
289 | local word = str:sub(i, x - 1)
290 | if not literals[word] then
291 | decode_error(str, i, "invalid literal '" .. word .. "'")
292 | end
293 | return literal_map[word], x
294 | end
295 |
296 |
297 | local function parse_array(str, i)
298 | local res = {}
299 | local n = 1
300 | i = i + 1
301 | while 1 do
302 | local x
303 | i = next_char(str, i, space_chars, true)
304 | -- Empty / end of array?
305 | if str:sub(i, i) == "]" then
306 | i = i + 1
307 | break
308 | end
309 | -- Read token
310 | x, i = parse(str, i)
311 | res[n] = x
312 | n = n + 1
313 | -- Next token
314 | i = next_char(str, i, space_chars, true)
315 | local chr = str:sub(i, i)
316 | i = i + 1
317 | if chr == "]" then break end
318 | if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
319 | end
320 | return res, i
321 | end
322 |
323 |
324 | local function parse_object(str, i)
325 | local res = {}
326 | i = i + 1
327 | while 1 do
328 | local key, val
329 | i = next_char(str, i, space_chars, true)
330 | -- Empty / end of object?
331 | if str:sub(i, i) == "}" then
332 | i = i + 1
333 | break
334 | end
335 | -- Read key
336 | if str:sub(i, i) ~= '"' then
337 | decode_error(str, i, "expected string for key")
338 | end
339 | key, i = parse(str, i)
340 | -- Read ':' delimiter
341 | i = next_char(str, i, space_chars, true)
342 | if str:sub(i, i) ~= ":" then
343 | decode_error(str, i, "expected ':' after key")
344 | end
345 | i = next_char(str, i + 1, space_chars, true)
346 | -- Read value
347 | val, i = parse(str, i)
348 | -- Set
349 | res[key] = val
350 | -- Next token
351 | i = next_char(str, i, space_chars, true)
352 | local chr = str:sub(i, i)
353 | i = i + 1
354 | if chr == "}" then break end
355 | if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
356 | end
357 | return res, i
358 | end
359 |
360 |
361 | local char_func_map = {
362 | [ '"' ] = parse_string,
363 | [ "0" ] = parse_number,
364 | [ "1" ] = parse_number,
365 | [ "2" ] = parse_number,
366 | [ "3" ] = parse_number,
367 | [ "4" ] = parse_number,
368 | [ "5" ] = parse_number,
369 | [ "6" ] = parse_number,
370 | [ "7" ] = parse_number,
371 | [ "8" ] = parse_number,
372 | [ "9" ] = parse_number,
373 | [ "-" ] = parse_number,
374 | [ "t" ] = parse_literal,
375 | [ "f" ] = parse_literal,
376 | [ "n" ] = parse_literal,
377 | [ "[" ] = parse_array,
378 | [ "{" ] = parse_object,
379 | }
380 |
381 |
382 | parse = function(str, idx)
383 | local chr = str:sub(idx, idx)
384 | local f = char_func_map[chr]
385 | if f then
386 | return f(str, idx)
387 | end
388 | decode_error(str, idx, "unexpected character '" .. chr .. "'")
389 | end
390 |
391 |
392 | --- Decode a JSON string and return the Lua data.
393 | -- @param str The encoded JSON string to decode.
394 | -- @return The decoded Lua data.
395 | function json.decode(str)
396 | if type(str) ~= "string" then
397 | error("expected argument of type string, got " .. type(str))
398 | end
399 | local res, idx = parse(str, next_char(str, 1, space_chars, true))
400 | idx = next_char(str, idx, space_chars, true)
401 | if idx <= #str then
402 | decode_error(str, idx, "trailing garbage")
403 | end
404 | return res
405 | end
406 |
407 |
408 | return json
409 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | 
3 |
4 | # Nakama
5 |
6 | > Lua client for Nakama server written in Lua 5.1.
7 |
8 | [Nakama](https://github.com/heroiclabs/nakama) is an open-source server designed to power modern games and apps. Features include user accounts, chat, social, matchmaker, realtime multiplayer, and much [more](https://heroiclabs.com).
9 |
10 | This client implements the full API and socket options with the server. It's written in Lua 5.1 to be compatible with Lua based game engines.
11 |
12 | Full documentation is available [here](https://heroiclabs.com/docs/nakama/client-libraries/defold/index.html).
13 |
14 | ## Getting Started
15 |
16 | You'll need to setup the server and database before you can connect with the client. The simplest way is to use Docker but have a look at the [server documentation](https://github.com/heroiclabs/nakama#getting-started) for other options.
17 |
18 | 1. Install and run the servers. Follow these [instructions](https://heroiclabs.com/docs/nakama/getting-started/install/docker/).
19 |
20 | 2. Add the client to your project.
21 |
22 | * In Defold projects you need to add the URL of a [stable release](https://github.com/heroiclabs/nakama-defold/releases) or the [latest development version](https://github.com/heroiclabs/nakama-defold/archive/master.zip) as a library dependency to `game.project`. The client will now show up in `nakama` folder in your project.
23 |
24 | 3. Add dependencies to your project. In Defold projects you need to add one of the following dependencies to game.project:
25 |
26 | * https://github.com/defold/extension-websocket/archive/2.1.0.zip (Defold version <= 1.2.181)
27 | * https://github.com/defold/extension-websocket/archive/3.0.0.zip (Defold version >= 1.2.182)
28 |
29 | 4. Use the connection credentials to initialise the nakama client.
30 |
31 | ```lua
32 | local defold = require "nakama.engine.defold"
33 | local nakama = require "nakama.nakama"
34 | local config = {
35 | host = "127.0.0.1",
36 | port = 7350,
37 | use_ssl = false,
38 | username = "defaultkey",
39 | password = "",
40 | engine = defold,
41 | timeout = 10, -- connection timeout in seconds
42 | }
43 | local client = nakama.create_client(config)
44 | ```
45 |
46 | 5. (Optional) Nakama uses base64 decoding for session the session tokens and both base64 encoding and decoding of match data. The default base64 encoder and decoder is written in Lua. To increase performance of the base64 encode and decode steps it is possible to use a base64 encoder written in C. In Defold projects you need to add the following dependency to game.project:
47 |
48 | * https://github.com/defold/extension-crypt/archive/refs/tags/1.0.2.zip
49 |
50 |
51 | ## Usage
52 |
53 | The client has many methods to execute various features in the server or open realtime socket connections with the server.
54 |
55 |
56 | ### Authenticate
57 |
58 | There's a variety of ways to [authenticate](https://heroiclabs.com/docs/authentication) with the server. Authentication can create a user if they don't already exist with those credentials. It's also easy to authenticate with a social profile from Google Play Games, Facebook, Game Center, etc.
59 |
60 | ```lua
61 | local client = nakama.create_client(config)
62 |
63 | local email = "super@heroes.com"
64 | local password = "batsignal"
65 | local session = client.authenticate_email(email, password)
66 | pprint(session)
67 | ```
68 |
69 | > _Note_: see [Requests](#Requests) section below for running this snippet (a)synchronously.
70 |
71 | ### Sessions
72 |
73 | When authenticated the server responds with an auth token (JWT) which can be used to authenticate API requests. The token contains useful properties and gets deserialized into a `session` table.
74 |
75 | ```lua
76 | local client = nakama.create_client(config)
77 |
78 | local session = client.authenticate_email(email, password)
79 |
80 | print(session.created)
81 | print(session.token) -- raw JWT token
82 | print(session.expires)
83 | print(session.user_id)
84 | print(session.username)
85 | print(session.refresh_token) -- raw JWT token for use when refreshing the session
86 | print(session.refresh_token_expires)
87 | print(session.refresh_token_user_id)
88 | print(session.refresh_token_username)
89 |
90 | -- Use the token to authenticate future API requests
91 | nakama.set_bearer_token(client, session.token)
92 |
93 | -- Use the refresh token to refresh the authentication token
94 | nakama.session_refresh(client, session.refresh_token)
95 | ```
96 |
97 | It is recommended to store the auth token from the session and check at startup if it has expired. If the token has expired you must reauthenticate. If the token is about to expire it has to be refreshed. The expiry time of the token can be changed as a setting in the server. You can store the session using `session.store(session)` and later restored it using `session.restore()`:
98 |
99 | ```lua
100 | local nakama_session = require "nakama.session"
101 |
102 | local client = nakama.create_client(config)
103 |
104 | -- restore a session
105 | local session = nakama_session.restore()
106 |
107 | if session and nakama_session.is_token_expired_soon(session) and not nakama.is_refresh_token_expired(session) then
108 | print("Session has expired or is about to expire. Refreshing.")
109 | session = nakama.session_refresh(client, session.refresh_token)
110 | nakama_session.store(session)
111 | elseif not session or nakama_session.is_refresh_token_expired(session) then
112 | print("Session does not exist or it has expired. Must reauthenticate.")
113 | session = client.authenticate_email("bjorn@defold.se", "foobar123", nil, true, "britzl")
114 | nakama_session.store(session)
115 | end
116 | client.set_bearer_token(session.token)
117 | ```
118 |
119 | ### Requests
120 |
121 | The client includes lots of built-in APIs for various features of the game server. These can be accessed with the methods which either use a callback function to return a result (ie. asynchronous) or yield until a result is received (ie. synchronous and must be run within a Lua coroutine).
122 |
123 | ```lua
124 | local client = nakama.create_client(config)
125 |
126 | -- using a callback
127 | client.get_account(function(account)
128 | print(account.user.id);
129 | print(account.user.username);
130 | print(account.wallet);
131 | end)
132 |
133 | -- if run from within a coroutine
134 | local account = client.get_account()
135 | print(account.user.id);
136 | print(account.user.username);
137 | print(account.wallet);
138 | ```
139 |
140 | The Nakama client provides a convenience function for creating and starting a coroutine to run multiple requests synchronously one after the other:
141 |
142 | ```lua
143 | nakama.sync(function()
144 | local account = client.get_account()
145 | local result = client.update_account(request)
146 | end)
147 | ```
148 |
149 |
150 | ### Retries
151 | Nakama has a global and per-request retry configuration to control how failed API calls are retried.
152 |
153 | ```lua
154 | local retries = require "nakama.util.retries"
155 |
156 | -- use a global retry policy with 5 attempts with 1 second intervals
157 | local config = {
158 | host = "127.0.0.1",
159 | port = 7350,
160 | username = "defaultkey",
161 | password = "",
162 | retry_policy = retries.fixed(5, 1),
163 | engine = defold,
164 | }
165 | local client = nakama.create_client(config)
166 |
167 | -- use a retry policy specifically for this request
168 | -- 5 retries at intervals increasing by 1 second between attempts (eg 1s, 2s, 3s, 4s, 5s)
169 | nakama.list_friends(client, 10, 0, "", retries.incremental(5, 1))
170 | ```
171 |
172 |
173 | ### Cancelling requests
174 | Create a cancellation token and pass that with a request to cancel the request before it has completed.
175 |
176 | ```lua
177 | -- use a global retry policy with 5 attempts with 1 second intervals
178 | local config = {
179 | host = "127.0.0.1",
180 | port = 7350,
181 | username = "defaultkey",
182 | password = "",
183 | retry_policy = retries.fixed(5, 1),
184 | engine = defold,
185 | }
186 | local client = nakama.create_client(config)
187 |
188 | -- create a cancellation token
189 | local token = nakama.cancellation_token()
190 |
191 | -- start a request and proivide the cancellation token
192 | nakama.list_friends(client, 10, 0, "", nil, callback, token)
193 |
194 | -- immediately cancel the request without waiting for the request callback to be invoked
195 | nakama.cancel(token)
196 | ```
197 |
198 |
199 | ### Socket
200 |
201 | You can connect to the server over a realtime WebSocket connection to send and receive chat messages, get notifications, and matchmake into a multiplayer match.
202 |
203 | You first need to create a realtime socket to the server:
204 |
205 | ```lua
206 | local client = nakama.create_client(config)
207 |
208 | -- create socket
209 | local socket = client.create_socket()
210 |
211 | nakama.sync(function()
212 | -- connect
213 | local ok, err = socket.connect()
214 | end)
215 | ```
216 |
217 | Then proceed to join a chat channel and send a message:
218 |
219 | ```lua
220 | -- send channel join message
221 | local channel_id = "pineapple-pizza-lovers-room"
222 | local result = socket.channel_join(socket, 1, channel_id, false, false)
223 |
224 | -- send channel messages
225 | local result = socket.channel_message_send(channel_id, "Pineapple doesn't belong on a pizza!")
226 | ```
227 |
228 |
229 | #### Handle events
230 |
231 | A client socket has event listeners which are called on various events received from the server. Example:
232 |
233 | ```lua
234 | socket.on_disconnect(function(message)
235 | print("Disconnected!")
236 | end)
237 | ```
238 |
239 | Available listeners:
240 |
241 | * `on_disconnect` - Handles an event for when the client is disconnected from the server.
242 | * `on_channel_presence_event`
243 | * `on_match_presence_event`
244 | * `on_match_data`
245 | * `on_match`
246 | * `on_matchmaker_matched`
247 | * `on_notifications`
248 | * `on_party_presence_event`
249 | * `on_party`
250 | * `on_party_data`
251 | * `on_party_join_request`
252 | * `on_status_presence_event`
253 | * `on_status`
254 | * `on_stream_data`
255 | * `on_error`
256 | * `on_channel_message`
257 | * `on_channel_message`
258 |
259 |
260 |
261 | ### Match data
262 |
263 | Nakama [supports any binary content](https://heroiclabs.com/docs/gameplay-multiplayer-realtime/#send-data-messages) in `data` attribute of a match message. Regardless of your data type, the server **only accepts base64-encoded data**, so make sure you don't post plain-text data or even JSON, or Nakama server will claim the data malformed and disconnect your client (set server logging to `debug` to detect these events).
264 |
265 | Nakama will automatically base64 encode your match data if the message was created using `nakama.create_match_data_message()`. Nakama will also automatically base64 decode any received match data before calling the `on_matchdata` listener.
266 |
267 | ```lua
268 |
269 | local json = require "nakama.util.json"
270 |
271 | local match_id = "..."
272 | local op_code = 1
273 | local data = json.encode({
274 | dest_x = 1.0,
275 | dest_y = 0.1,
276 | })
277 |
278 | -- send a match data message. The data will be automatically base64 encoded.
279 | socket.match_data(match_id, op_code, data)
280 | ```
281 |
282 | In a relayed multiplayer, you'll be receiving other clients' messages. The client has already base64 decoded the message data before sending it to the `on_matchdata` listener. If the data was JSON encoded, like in the example above, you need to decode it yourself:
283 |
284 | ```lua
285 | socket.on_matchdata(function(message)
286 | local match_data = message.match_data
287 | local data = json.decode(match_data.data)
288 | pprint(data) -- gameplay coordinates from the example above
289 | end)
290 | ```
291 |
292 | Messages initiated _by the server_ in an authoritative match will come as valid JSON by default.
293 |
294 |
295 | # Satori
296 |
297 | > Lua client for Satori written in Lua 5.1.
298 |
299 | [Satori](https://heroiclabs.com/satori/) is a liveops server for games that powers actionable analytics, A/B testing and remote configuration. Use the Satori Defold client to communicate with Satori from within your Defold game.
300 |
301 | ## Getting started
302 |
303 | Create a Satori client using the API key from the Satori dashboard.
304 |
305 |
306 | ```lua
307 | local config = {
308 | host = "myhost.com",
309 | api_key = "my-api-key",
310 | use_ssl = true,
311 | port = 443,
312 | retry_policy = retries.incremental(5, 1),
313 | engine = defold,
314 | }
315 | local client = satori.create_client(config)
316 | ```
317 |
318 | Then authenticate to obtain your session:
319 |
320 | ```lua
321 | satori.sync(function()
322 | local uuid = defold.uuid()
323 | local result = client.authenticate(nil, nil, uuid)
324 | if not result.token then
325 | error("Unable to login")
326 | return
327 | end
328 | client.set_bearer_token(result.token)
329 | end)
330 | ```
331 |
332 | Using the client you can get any experiments or feature flags, the user belongs to.
333 |
334 | ```lua
335 | satori.sync(function()
336 | local experiments = satori.get_experiments(client)
337 | pprint(experiments)
338 |
339 | local flags = satori.get_flags(client)
340 | pprint(flags)
341 | end)
342 | ```
343 |
344 |
345 | # Contribute
346 |
347 | The development roadmap is managed as GitHub issues and pull requests are welcome. If you're interested to enhance the code please open an issue to discuss the changes or drop in and discuss it in the [community forum](https://forum.heroiclabs.com).
348 |
349 |
350 | ## Run tests
351 |
352 | Unit tests can be found in the `tests` folder. Run them using [Telescope](https://github.com/defold/telescope) (fork which supports Lua 5.3+):
353 |
354 | ```
355 | ./tsc -f test/test_nakama.lua test/test_satori.lua test/test_socket.lua test/test_session.lua
356 | ```
357 |
358 | ## Generate Docs
359 |
360 | API docs are generated with Ldoc and deployed to GitHub pages.
361 |
362 | When changing the API comments, rerun Ldoc and commit the changes in `docs/*`.
363 |
364 | Note: Comments for `nakama/nakama.lua` must be made in `codegen/main.go`.
365 |
366 | To run Ldoc:
367 |
368 | ```
369 | # in the project root, generate nakama.lua
370 | # requires go and https://github.com/heroiclabs/nakama to be checked out
371 | go run codegen/main.go -output nakama/nakama.lua ../nakama/apigrpc/apigrpc.swagger.json
372 |
373 | # install ldoc (mac)
374 | brew install luarocks
375 | luarocks install ldoc
376 |
377 | # run ldoc
378 | doc . -d docs
379 | ```
380 |
381 |
382 | ## Generate code
383 |
384 | Refer to instructions in the [codegen folder](/codegen).
385 |
386 |
387 | ## Adapting to other engines
388 |
389 | Adapting the Nakama and Satori Defold clients to another Lua based engine should be as easy as providing another engine module when configuring the Nakama client:
390 |
391 | ```lua
392 | -- nakama
393 | local myengine = require "nakama.engine.myengine"
394 | local nakama = require "nakama.nakama"
395 | local nakama_config = {
396 | engine = myengine,
397 | }
398 | local nakama_client = nakama.create_client(nakama_config)
399 |
400 | -- satori
401 | local myengine = require "nakama.engine.myengine"
402 | local satori = require "satori.satori"
403 | local satori_config = {
404 | engine = myengine,
405 | }
406 | local satori_client = satori.create_client(satori_config)
407 | ```
408 |
409 | The engine module must provide the following functions:
410 |
411 | * `http(config, url_path, query_params, method, post_data, cancellation_token, callback)` - Make HTTP request.
412 | * `config` - Config table passed to `nakama.create()` or `satori.create()`
413 | * `url_path` - Path to append to the base uri
414 | * `query_params` - Key-value pairs to use as URL query parameters
415 | * `method` - "GET", "POST"
416 | * `post_data` - Data to post
417 | * `cancellation_token` - Check if `cancellation_token.cancelled` is true
418 | * `callback` - Function to call with result (response)
419 |
420 | * `socket_create(config, on_message)` - Create socket. Must return socket instance (table with engine specific socket state).
421 | * `config` - Config table passed to `nakama.create()` or `satori.create()
422 | * `on_message` - Function to call when a message is sent from the server
423 |
424 | * `socket_connect(socket, callback)` - Connect socket.
425 | * `socket` - Socket instance returned from `socket_create()`
426 | * `callback` - Function to call with result (ok, err)
427 |
428 | * `socket_disconnect(socket)` - Disonnect socket.
429 | * `socket` - Socket instance returned from `socket_create()`
430 |
431 | * `socket_send(socket, message)` - Send message on socket.
432 | * `socket` - Socket instance returned from `socket_create()`
433 | * `message` - Message to send
434 |
435 | * `uuid()` - Create a UUID
436 |
437 |
438 | # Licenses
439 |
440 | This project is licensed under the [Apache-2 License](https://github.com/heroiclabs/nakama-defold/blob/master/LICENSE).
441 |
--------------------------------------------------------------------------------
/codegen/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.realtime;
21 |
22 | import "google/protobuf/timestamp.proto";
23 | import "google/protobuf/wrappers.proto";
24 | import "api/api.proto";
25 |
26 | option go_package = "github.com/heroiclabs/nakama-common/rtapi";
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 | Channel channel = 2;
40 | // Join a realtime chat channel.
41 | ChannelJoin channel_join = 3;
42 | // Leave a realtime chat channel.
43 | ChannelLeave channel_leave = 4;
44 | // An incoming message on a realtime chat channel.
45 | api.ChannelMessage channel_message = 5;
46 | // An acknowledgement received in response to sending a message on a chat channel.
47 | ChannelMessageAck channel_message_ack = 6;
48 | // Send a message to a realtime chat channel.
49 | ChannelMessageSend channel_message_send = 7;
50 | // Update a message previously sent to a realtime chat channel.
51 | ChannelMessageUpdate channel_message_update = 8;
52 | // Remove a message previously sent to a realtime chat channel.
53 | ChannelMessageRemove channel_message_remove = 9;
54 | // Presence update for a particular realtime chat channel.
55 | ChannelPresenceEvent channel_presence_event = 10;
56 | // Describes an error which occurred on the server.
57 | Error error = 11;
58 | // Incoming information about a realtime match.
59 | Match match = 12;
60 | // A client to server request to create a realtime match.
61 | MatchCreate match_create = 13;
62 | // Incoming realtime match data delivered from the server.
63 | MatchData match_data = 14;
64 | // A client to server request to send data to a realtime match.
65 | MatchDataSend match_data_send = 15;
66 | // A client to server request to join a realtime match.
67 | MatchJoin match_join = 16;
68 | // A client to server request to leave a realtime match.
69 | MatchLeave match_leave = 17;
70 | // Presence update for a particular realtime match.
71 | MatchPresenceEvent match_presence_event = 18;
72 | // Submit a new matchmaking process request.
73 | MatchmakerAdd matchmaker_add = 19;
74 | // A successful matchmaking result.
75 | MatchmakerMatched matchmaker_matched = 20;
76 | // Cancel a matchmaking process using a ticket.
77 | MatchmakerRemove matchmaker_remove = 21;
78 | // A response from starting a new matchmaking process.
79 | MatchmakerTicket matchmaker_ticket = 22;
80 | // Notifications send by the server.
81 | Notifications notifications = 23;
82 | // RPC call or response.
83 | api.Rpc rpc = 24;
84 | // An incoming status snapshot for some set of users.
85 | Status status = 25;
86 | // Start following some set of users to receive their status updates.
87 | StatusFollow status_follow = 26;
88 | // An incoming status update.
89 | StatusPresenceEvent status_presence_event = 27;
90 | // Stop following some set of users to no longer receive their status updates.
91 | StatusUnfollow status_unfollow = 28;
92 | // Set the user's own status.
93 | StatusUpdate status_update = 29;
94 | // A data message delivered over a stream.
95 | StreamData stream_data = 30;
96 | // Presence update for a particular stream.
97 | StreamPresenceEvent stream_presence_event = 31;
98 | // Application-level heartbeat and connection check.
99 | Ping ping = 32;
100 | // Application-level heartbeat and connection check response.
101 | Pong pong = 33;
102 | // Incoming information about a party.
103 | Party party = 34;
104 | // Create a party.
105 | PartyCreate party_create = 35;
106 | // Join a party, or request to join if the party is not open.
107 | PartyJoin party_join = 36;
108 | // Leave a party.
109 | PartyLeave party_leave = 37;
110 | // Promote a new party leader.
111 | PartyPromote party_promote = 38;
112 | // Announcement of a new party leader.
113 | PartyLeader party_leader = 39;
114 | // Accept a request to join.
115 | PartyAccept party_accept = 40;
116 | // Kick a party member, or decline a request to join.
117 | PartyRemove party_remove = 41;
118 | // End a party, kicking all party members and closing it.
119 | PartyClose party_close = 42;
120 | // Request a list of pending join requests for a party.
121 | PartyJoinRequestList party_join_request_list = 43;
122 | // Incoming notification for one or more new presences attempting to join the party.
123 | PartyJoinRequest party_join_request = 44;
124 | // Begin matchmaking as a party.
125 | PartyMatchmakerAdd party_matchmaker_add = 45;
126 | // Cancel a party matchmaking process using a ticket.
127 | PartyMatchmakerRemove party_matchmaker_remove = 46;
128 | // A response from starting a new party matchmaking process.
129 | PartyMatchmakerTicket party_matchmaker_ticket = 47;
130 | // Incoming party data delivered from the server.
131 | PartyData party_data = 48;
132 | // A client to server request to send data to a party.
133 | PartyDataSend party_data_send = 49;
134 | // Presence update for a particular party.
135 | PartyPresenceEvent party_presence_event = 50;
136 | }
137 | }
138 |
139 | // A realtime chat channel.
140 | message Channel {
141 | // The ID of the channel.
142 | string id = 1;
143 | // The users currently in the channel.
144 | repeated UserPresence presences = 2;
145 | // A reference to the current user's presence in the channel.
146 | UserPresence 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 | // Join operation for a realtime chat channel.
158 | message ChannelJoin {
159 | // The type of chat channel.
160 | enum Type {
161 | // Default case. Assumed as ROOM type.
162 | TYPE_UNSPECIFIED = 0;
163 | // A room which anyone can join to chat.
164 | ROOM = 1;
165 | // A private channel for 1-on-1 chat.
166 | DIRECT_MESSAGE = 2;
167 | // A channel for group chat.
168 | GROUP = 3;
169 | }
170 |
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 ChannelLeave {
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 ChannelMessageAck {
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 last updated.
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 ChannelMessageSend {
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 ChannelMessageUpdate {
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 ChannelMessageRemove {
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 ChannelPresenceEvent {
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 UserPresence joins = 2;
245 | // Presences leaving the channel as part of this event, if any.
246 | repeated UserPresence 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 | // A logical error which may occur on the server.
258 | message Error {
259 | // The selection of possible error codes.
260 | enum Code {
261 | // An unexpected result from the server.
262 | RUNTIME_EXCEPTION = 0;
263 | // The server received a message which is not recognised.
264 | UNRECOGNIZED_PAYLOAD = 1;
265 | // A message was expected but contains no content.
266 | MISSING_PAYLOAD = 2;
267 | // Fields in the message have an invalid format.
268 | BAD_INPUT = 3;
269 | // The match id was not found.
270 | MATCH_NOT_FOUND = 4;
271 | // The match join was rejected.
272 | MATCH_JOIN_REJECTED = 5;
273 | // The runtime function does not exist on the server.
274 | RUNTIME_FUNCTION_NOT_FOUND = 6;
275 | // The runtime function executed with an error.
276 | RUNTIME_FUNCTION_EXCEPTION = 7;
277 | }
278 |
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 Match {
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 UserPresence presences = 5;
299 | // A reference to the current user's presence in the match.
300 | UserPresence self = 6;
301 | }
302 |
303 | // Create a new realtime match.
304 | message MatchCreate {
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 MatchData {
311 | // The match unique ID.
312 | string match_id = 1;
313 | // A reference to the user presence that sent this data, if any.
314 | UserPresence 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 MatchDataSend {
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 UserPresence 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 MatchJoin {
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 MatchLeave {
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 MatchPresenceEvent {
357 | // The match unique ID.
358 | string match_id = 1;
359 | // User presences that have just joined the match.
360 | repeated UserPresence joins = 2;
361 | // User presences that have just left the match.
362 | repeated UserPresence leaves = 3;
363 | }
364 |
365 | // Start a new matchmaking process.
366 | message MatchmakerAdd {
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 | // A successful matchmaking result.
382 | message MatchmakerMatched {
383 | message MatchmakerUser {
384 | // User info.
385 | UserPresence presence = 1;
386 | // Party identifier, if this user was matched as a party member.
387 | string party_id = 2;
388 | // String properties.
389 | map string_properties = 5;
390 | // Numeric properties.
391 | map numeric_properties = 6;
392 | }
393 |
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 MatchmakerUser users = 4;
405 | // A reference to the current user and their properties.
406 | MatchmakerUser self = 5;
407 | }
408 |
409 | // Cancel an existing ongoing matchmaking process.
410 | message MatchmakerRemove {
411 | // The ticket to cancel.
412 | string ticket = 1;
413 | }
414 |
415 | // A ticket representing a new matchmaking process.
416 | message MatchmakerTicket {
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 Notifications {
423 | // Collection of notifications.
424 | repeated api.Notification notifications = 1;
425 | }
426 |
427 | // Incoming information about a party.
428 | message Party {
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 | UserPresence self = 4;
437 | // Leader.
438 | UserPresence leader = 5;
439 | // All current party members.
440 | repeated UserPresence presences = 6;
441 | }
442 |
443 | // Create a party.
444 | message PartyCreate {
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 PartyJoin {
453 | // Party ID to join.
454 | string party_id = 1;
455 | }
456 |
457 | // Leave a party.
458 | message PartyLeave {
459 | // Party ID to leave.
460 | string party_id = 1;
461 | }
462 |
463 | // Promote a new party leader.
464 | message PartyPromote {
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 | UserPresence presence = 2;
469 | }
470 |
471 | // Announcement of a new party leader.
472 | message PartyLeader {
473 | // Party ID to announce the new leader for.
474 | string party_id = 1;
475 | // The presence of the new party leader.
476 | UserPresence presence = 2;
477 | }
478 |
479 | // Accept a request to join.
480 | message PartyAccept {
481 | // Party ID to accept a join request for.
482 | string party_id = 1;
483 | // The presence to accept as a party member.
484 | UserPresence presence = 2;
485 | }
486 |
487 | // Kick a party member, or decline a request to join.
488 | message PartyRemove {
489 | // Party ID to remove/reject from.
490 | string party_id = 1;
491 | // The presence to remove or reject.
492 | UserPresence presence = 2;
493 | }
494 |
495 | // End a party, kicking all party members and closing it.
496 | message PartyClose {
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 PartyJoinRequestList {
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 PartyJoinRequest {
509 | // Party ID these presences are attempting to join.
510 | string party_id = 1;
511 | // Presences attempting to join.
512 | repeated UserPresence presences = 2;
513 | }
514 |
515 | // Begin matchmaking as a party.
516 | message PartyMatchmakerAdd {
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 PartyMatchmakerRemove {
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 PartyMatchmakerTicket {
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 PartyData {
551 | // The party ID.
552 | string party_id = 1;
553 | // A reference to the user presence that sent this data, if any.
554 | UserPresence 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 PartyDataSend {
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 PartyPresenceEvent {
573 | // The party ID.
574 | string party_id = 1;
575 | // User presences that have just joined the party.
576 | repeated UserPresence joins = 2;
577 | // User presences that have just left the party.
578 | repeated UserPresence leaves = 3;
579 | }
580 |
581 |
582 | // Application-level heartbeat and connection check.
583 | message Ping {}
584 |
585 | // Application-level heartbeat and connection check response.
586 | message Pong {}
587 |
588 | // A snapshot of statuses for some set of users.
589 | message Status {
590 | // User statuses.
591 | repeated UserPresence presences = 1;
592 | }
593 |
594 | // Start receiving status updates for some set of users.
595 | message StatusFollow {
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 StatusPresenceEvent {
604 | // New statuses for the user.
605 | repeated UserPresence joins = 2;
606 | // Previous statuses for the user.
607 | repeated UserPresence leaves = 3;
608 | }
609 |
610 | // Stop receiving status updates for some set of users.
611 | message StatusUnfollow {
612 | // Users to unfollow.
613 | repeated string user_ids = 1;
614 | }
615 |
616 | // Set the user's own status.
617 | message StatusUpdate {
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 Stream {
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 StreamData {
636 | // The stream this data message relates to.
637 | Stream stream = 1;
638 | // The sender, if any.
639 | UserPresence 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 StreamPresenceEvent {
648 | // The stream this event relates to.
649 | Stream stream = 1;
650 | // Presences joining the stream as part of this event, if any.
651 | repeated UserPresence joins = 2;
652 | // Presences leaving the stream as part of this event, if any.
653 | repeated UserPresence leaves = 3;
654 | }
655 |
656 | // A user session associated to a stream, usually through a list operation or a join/leave event.
657 | message UserPresence {
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 |
--------------------------------------------------------------------------------