├── docs ├── CNAME ├── modules │ ├── nakama.util.async.html │ ├── nakama.util.log.html │ ├── nakama.util.base64.html │ ├── nakama.session.html │ ├── nakama.util.json.html │ ├── nakama.engine.defold.html │ └── nakama.util.uuid.html ├── index.html └── ldoc.css ├── input └── game.input_binding ├── nakama ├── util │ ├── time.lua │ ├── log.lua │ ├── async.lua │ ├── b64.lua │ ├── retries.lua │ ├── uri.lua │ ├── uuid.lua │ └── json.lua ├── engine │ ├── test.lua │ └── defold.lua └── session.lua ├── example ├── example.script ├── example.collection ├── example-satori.script └── example-nakama.script ├── .gitignore ├── config.ld ├── codegen ├── README.md ├── generate.sh ├── template-satori.go ├── template-nakama.go ├── template-common.go ├── generate-rest.go ├── generate-nakama-realtime.py └── realtime.proto ├── game.project ├── .github └── workflows │ └── test.yml ├── test ├── test_session.lua ├── test_retries.lua ├── test_nakama.lua ├── test_satori.lua └── test_socket.lua ├── CHANGELOG.md ├── LICENSE └── README.md /docs/CNAME: -------------------------------------------------------------------------------- 1 | defold.docs.heroiclabs.com -------------------------------------------------------------------------------- /input/game.input_binding: -------------------------------------------------------------------------------- 1 | mouse_trigger { 2 | input: MOUSE_BUTTON_LEFT 3 | action: "touch" 4 | } 5 | -------------------------------------------------------------------------------- /nakama/util/time.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | function M.now() 4 | return os.date("%Y-%m-%dT%H:%M:%SZ") 5 | end 6 | 7 | return M -------------------------------------------------------------------------------- /example/example.script: -------------------------------------------------------------------------------- 1 | function init(self) 2 | msg.post("#example-satori", "run") 3 | --msg.post("#example-nakama", "run") 4 | end 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.internal 2 | /build 3 | .externalToolBuilders 4 | .DS_Store 5 | Thumbs.db 6 | .lock-wscript 7 | *.pyc 8 | .project 9 | .cproject 10 | builtins 11 | telescope.lua 12 | telescope/compat_env.lua 13 | tsc 14 | /.editor_settings -------------------------------------------------------------------------------- /config.ld: -------------------------------------------------------------------------------- 1 | file = { "nakama" } 2 | 3 | project = "Nakama Defold" 4 | title = "SDK API documentation" 5 | package = "nakama" 6 | description = "Nakama client SDK for Defold" 7 | full_description = "Developed by The Defold Foundation and maintained by Heroic Labs." 8 | -------------------------------------------------------------------------------- /codegen/README.md: -------------------------------------------------------------------------------- 1 | Generates Lua code from the Nakama and Satori API definitions (swagger and protobuf). 2 | 3 | ## Usage 4 | 5 | Generate Lua bindings for the Nakama and Satori REST APIs and the Nakama realtime API: 6 | 7 | ```shell 8 | ./generate.sh 9 | ``` 10 | -------------------------------------------------------------------------------- /game.project: -------------------------------------------------------------------------------- 1 | [library] 2 | include_dirs = nakama,satori 3 | 4 | [project] 5 | title = Nakama 6 | dependencies#0 = https://github.com/defold/extension-websocket/archive/3.0.0.zip 7 | 8 | [bootstrap] 9 | main_collection = /example/example.collectionc 10 | 11 | [script] 12 | shared_state = 1 13 | 14 | -------------------------------------------------------------------------------- /codegen/generate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | go run generate-rest.go template-satori.go template-common.go satori.swagger.json > ../satori/satori.lua 4 | go run generate-rest.go template-nakama.go template-common.go apigrpc.swagger.json > ../nakama/nakama.lua 5 | python generate-nakama-realtime.py realtime.proto api.proto ../nakama/socket.lua 6 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v3 11 | name: Checkout project 12 | 13 | - name: Install Lua 14 | run: sudo apt-get install lua5.1 lua-check luarocks 15 | 16 | - name: Setup telescope 17 | run: | 18 | wget -O telescope.zip https://github.com/defold/telescope/archive/refs/heads/master.zip 19 | unzip telescope.zip 20 | mv telescope-master/tsc . 21 | mv telescope-master/telescope.lua . 22 | mv telescope-master/telescope . 23 | chmod +x tsc 24 | ls -la 25 | 26 | - name: Run tests 27 | run: | 28 | lua -v 29 | ./tsc -f test/test_socket.lua test/test_nakama.lua test/test_satori.lua test/test_session.lua test/test_retries.lua -------------------------------------------------------------------------------- /nakama/util/log.lua: -------------------------------------------------------------------------------- 1 | --[[-- 2 | Nakama logging module. 3 | 4 | @module nakama.util.log 5 | ]] 6 | 7 | 8 | local M = {} 9 | 10 | local function noop() end 11 | 12 | 13 | --- Silence all logging. 14 | function M.silent() 15 | M.log = noop 16 | end 17 | 18 | 19 | --- Print all log messages to the default system output. 20 | function M.print() 21 | M.log = print 22 | end 23 | 24 | 25 | -- Format all log message before print to the default system output. 26 | function M.format() 27 | M.log = function(fmt, ...) 28 | print(string.format(fmt, ...)) 29 | end 30 | end 31 | 32 | 33 | --- Set a custom log function. 34 | -- @param fn The custom log function. 35 | function M.custom(fn) 36 | M.log = fn 37 | end 38 | 39 | 40 | M.silent() 41 | 42 | 43 | setmetatable(M, { 44 | __call = function(t, ...) 45 | M.log(...) 46 | end 47 | }) 48 | 49 | 50 | return M 51 | -------------------------------------------------------------------------------- /nakama/util/async.lua: -------------------------------------------------------------------------------- 1 | --[[-- 2 | Run functions asynchronously. 3 | 4 | @module nakama.util.async 5 | ]] 6 | 7 | local M = {} 8 | 9 | local unpack = _G.unpack or table.unpack 10 | 11 | 12 | --- Execute a function asynchronously as a coroutines and return the result. 13 | -- @param fn The function to execute. 14 | -- @param ... Function params. 15 | -- @return The result of executing the function. 16 | function M.async(fn, ...) 17 | assert(fn) 18 | local co = coroutine.running() 19 | assert(co) 20 | local results = nil 21 | local state = "RUNNING" 22 | fn(function(...) 23 | results = { ... } 24 | if state == "YIELDED" then 25 | local ok, err = coroutine.resume(co) 26 | if not ok then print(err) end 27 | else 28 | state = "DONE" 29 | end 30 | end, ...) 31 | if state == "RUNNING" then 32 | state = "YIELDED" 33 | coroutine.yield() 34 | state = "DONE" -- not really needed 35 | end 36 | return unpack(results) 37 | end 38 | 39 | 40 | setmetatable(M, { 41 | __call = function(t, ...) 42 | return M.async(...) 43 | end 44 | }) 45 | 46 | 47 | return M 48 | -------------------------------------------------------------------------------- /test/test_session.lua: -------------------------------------------------------------------------------- 1 | local session = require "nakama.session" 2 | 3 | context("Session", function() 4 | before(function() end) 5 | after(function() end) 6 | 7 | test("It should be able to detect when a session has expired", function() 8 | local now = os.time() 9 | local expired_session = { 10 | expires = now - 1 11 | } 12 | local non_expired_session = { 13 | expires = now + 1 14 | } 15 | assert_true(session.expired(expired_session)) 16 | assert_false(session.expired(non_expired_session)) 17 | end) 18 | 19 | test("It should be able to create a session", function() 20 | local token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiI1MjJkMGI5MS00NmQzLTRjY2ItYmIwYS0wNTFjYjUyOGNhMDMiLCJ1c24iOiJicml0emwiLCJleHAiOjE2NjE1OTA5Nzl9.r3h4QraXsXl-XmGQueYecjeb6223vtd1s-Ak1K_FrGM" 21 | local refresh_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiI1MjJkMGI5MS00NmQzLTRjY2ItYmIwYS0wNTFjYjUyOGNhMDMiLCJ1c24iOiJicml0emwiLCJleHAiOjE2NjE1ODczNzl9.AWASctuZx9A8YliCLSj9jtOi4fuXUZaWtRdNz1mMEEw" 22 | local data = { 23 | token = token, 24 | refresh_token = refresh_token 25 | } 26 | 27 | local s = session.create(data) 28 | assert(s.created) 29 | assert_equal(s.token, token) 30 | assert_equal(s.expires, 1661590979) 31 | assert_equal(s.username, "britzl") 32 | assert_equal(s.user_id, "522d0b91-46d3-4ccb-bb0a-051cb528ca03") 33 | end) 34 | end) -------------------------------------------------------------------------------- /nakama/util/b64.lua: -------------------------------------------------------------------------------- 1 | --[[-- 2 | Base64 encode and decode data. 3 | 4 | From http://lua-users.org/wiki/BaseSixtyFour 5 | 6 | @module nakama.util.base64 7 | ]] 8 | 9 | -- 10 | 11 | local M = {} 12 | 13 | local b='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' 14 | 15 | 16 | --- Base64 encode a data string. 17 | -- @param data The data string to encode. 18 | -- @return The encoded data string. 19 | function M.encode(data) 20 | return ((data:gsub('.', function(x) 21 | local r,b='',x:byte() 22 | for i=8,1,-1 do r=r..(b%2^i-b%2^(i-1)>0 and '1' or '0') end 23 | return r; 24 | end)..'0000'):gsub('%d%d%d?%d?%d?%d?', function(x) 25 | if (#x < 6) then return '' end 26 | local c=0 27 | for i=1,6 do c=c+(x:sub(i,i)=='1' and 2^(6-i) or 0) end 28 | return b:sub(c+1,c+1) 29 | end)..({ '', '==', '=' })[#data%3+1]) 30 | end 31 | 32 | 33 | --- Decode a base64 encoded data string. 34 | -- @param data The encoded base64 string to decode. 35 | -- @return The decoded data string. 36 | function M.decode(data) 37 | data = string.gsub(data, '[^'..b..'=]', '') 38 | return (data:gsub('.', function(x) 39 | if (x == '=') then return '' end 40 | local r,f='',(b:find(x)-1) 41 | for i=6,1,-1 do r=r..(f%2^i-f%2^(i-1)>0 and '1' or '0') end 42 | return r; 43 | end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x) 44 | if (#x ~= 8) then return '' end 45 | local c=0 46 | for i=1,8 do c=c+(x:sub(i,i)=='1' and 2^(8-i) or 0) end 47 | return string.char(c) 48 | end)) 49 | end 50 | 51 | 52 | return M 53 | -------------------------------------------------------------------------------- /nakama/util/retries.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | 4 | --- Create a retry policy where the interval between attempts is exponentially increasing 5 | -- Example: interval = 0.5 and attempts = 5 gives retries: 0.5, 1.0, 2.0, 4.0, 8.0 6 | -- @param attempts The number of retry attempts 7 | -- @param interval (seconds) 8 | -- @return Retry intervals 9 | function M.exponential(attempts, interval) 10 | local delays = {} 11 | for i=1,attempts do 12 | delays[i] = (i > 1) and delays[i - 1] * 2 or interval 13 | end 14 | return delays 15 | end 16 | 17 | --- Create a retry policy where the interval between attempts is increasing 18 | -- Example: interval = 0.5 and attempts = 5 gives retries: 0.5, 1.0, 1.5, 2.0, 2.5 19 | -- @param attempts The number of retry attempts 20 | -- @param interval (seconds) 21 | -- @return Retry intervals 22 | function M.incremental(attempts, interval) 23 | local delays = {} 24 | for i=1,attempts do 25 | delays[i] = interval * i 26 | end 27 | return delays 28 | end 29 | 30 | --- Create a retry policy where the interval between attempts is fixed 31 | -- Example: interval = 0.5 and attempts = 5 gives retries: 0.5, 0.5, 0.5, 0.5, 0.5 32 | -- @param attempts The number of retry attempts 33 | -- @param interval (seconds) 34 | -- @return Retry intervals 35 | function M.fixed(attempts, interval) 36 | local delays = {} 37 | for i=1,attempts do 38 | delays[i] = interval 39 | end 40 | return delays 41 | end 42 | 43 | --- No retry policy 44 | function M.none() 45 | return {} 46 | end 47 | 48 | return M -------------------------------------------------------------------------------- /example/example.collection: -------------------------------------------------------------------------------- 1 | name: "example" 2 | scale_along_z: 0 3 | embedded_instances { 4 | id: "go" 5 | data: "components {\n" 6 | " id: \"example-nakama\"\n" 7 | " component: \"/example/example-nakama.script\"\n" 8 | " position {\n" 9 | " x: 0.0\n" 10 | " y: 0.0\n" 11 | " z: 0.0\n" 12 | " }\n" 13 | " rotation {\n" 14 | " x: 0.0\n" 15 | " y: 0.0\n" 16 | " z: 0.0\n" 17 | " w: 1.0\n" 18 | " }\n" 19 | " property_decls {\n" 20 | " }\n" 21 | "}\n" 22 | "components {\n" 23 | " id: \"example-satori\"\n" 24 | " component: \"/example/example-satori.script\"\n" 25 | " position {\n" 26 | " x: 0.0\n" 27 | " y: 0.0\n" 28 | " z: 0.0\n" 29 | " }\n" 30 | " rotation {\n" 31 | " x: 0.0\n" 32 | " y: 0.0\n" 33 | " z: 0.0\n" 34 | " w: 1.0\n" 35 | " }\n" 36 | " property_decls {\n" 37 | " }\n" 38 | "}\n" 39 | "components {\n" 40 | " id: \"example\"\n" 41 | " component: \"/example/example.script\"\n" 42 | " position {\n" 43 | " x: 0.0\n" 44 | " y: 0.0\n" 45 | " z: 0.0\n" 46 | " }\n" 47 | " rotation {\n" 48 | " x: 0.0\n" 49 | " y: 0.0\n" 50 | " z: 0.0\n" 51 | " w: 1.0\n" 52 | " }\n" 53 | " property_decls {\n" 54 | " }\n" 55 | "}\n" 56 | "" 57 | position { 58 | x: 0.0 59 | y: 0.0 60 | z: 0.0 61 | } 62 | rotation { 63 | x: 0.0 64 | y: 0.0 65 | z: 0.0 66 | w: 1.0 67 | } 68 | scale3 { 69 | x: 1.0 70 | y: 1.0 71 | z: 1.0 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/test_retries.lua: -------------------------------------------------------------------------------- 1 | local retries = require "nakama.util.retries" 2 | local log = require "nakama.util.log" 3 | log.print() 4 | 5 | context("Retries", function() 6 | 7 | before(function() end) 8 | after(function() end) 9 | 10 | test("create exponentially increasing intervals", function() 11 | local intervals = retries.exponential(5, 0.5) 12 | assert(#intervals == 5) 13 | assert(intervals[1] == 0.5) 14 | assert(intervals[2] == 1.0) 15 | assert(intervals[3] == 2.0) 16 | assert(intervals[4] == 4.0) 17 | assert(intervals[5] == 8.0) 18 | end) 19 | 20 | test("create incrementally increasing intervals", function() 21 | local intervals = retries.incremental(5, 0.5) 22 | assert(#intervals == 5) 23 | assert(intervals[1] == 0.5) 24 | assert(intervals[2] == 1.0) 25 | assert(intervals[3] == 1.5) 26 | assert(intervals[4] == 2.0) 27 | assert(intervals[5] == 2.5) 28 | end) 29 | 30 | test("create fixed intervals", function() 31 | local intervals = retries.fixed(5, 0.5) 32 | assert(#intervals == 5) 33 | assert(intervals[1] == 0.5) 34 | assert(intervals[2] == 0.5) 35 | assert(intervals[3] == 0.5) 36 | assert(intervals[4] == 0.5) 37 | assert(intervals[5] == 0.5) 38 | end) 39 | 40 | test("create no intervals", function() 41 | local intervals = retries.none() 42 | assert(#intervals == 0) 43 | intervals = retries.exponential(0, 0) 44 | assert(#intervals == 0) 45 | intervals = retries.incremental(0, 0) 46 | assert(#intervals == 0) 47 | intervals = retries.fixed(0, 0) 48 | assert(#intervals == 0) 49 | end) 50 | end) 51 | 52 | 53 | -------------------------------------------------------------------------------- /nakama/engine/test.lua: -------------------------------------------------------------------------------- 1 | local uuid = require "nakama.util.uuid" 2 | 3 | local M = {} 4 | 5 | ----------------- 6 | -- TEST HELPER -- 7 | ----------------- 8 | 9 | local http_request_response = {} 10 | local http_request_queue = {} 11 | local socket_send_queue = {} 12 | 13 | function M.set_http_response(path, response) 14 | assert(path, response) 15 | http_request_response[path] = response 16 | end 17 | 18 | function M.get_http_request() 19 | return table.remove(http_request_queue) 20 | end 21 | 22 | function M.get_socket_message() 23 | return table.remove(socket_send_queue) 24 | end 25 | 26 | function M.receive_socket_message(socket, message) 27 | socket.on_message(socket, message) 28 | end 29 | 30 | function M.reset() 31 | http_request_response = {} 32 | http_request_queue = {} 33 | socket_send_queue = {} 34 | end 35 | 36 | ---------------- 37 | -- ENGINE API -- 38 | ---------------- 39 | 40 | function M.uuid() 41 | return uuid("") 42 | end 43 | 44 | function M.http(config, url_path, query_params, method, post_data, retry_policy, cancellation_token, callback) 45 | local request = { 46 | config = config, 47 | url_path = url_path, 48 | query_params = query_params, 49 | method = method, 50 | post_data = post_data 51 | } 52 | table.insert(http_request_queue, request) 53 | 54 | local response = http_request_response[url_path] 55 | callback(response) 56 | end 57 | 58 | function M.socket_create(config, on_message) 59 | local socket = { 60 | on_message = on_message 61 | } 62 | return socket 63 | end 64 | 65 | function M.socket_connect(socket, callback) 66 | local result = true 67 | callback(result) 68 | end 69 | 70 | function M.socket_disconnect(socket) 71 | end 72 | 73 | function M.socket_send(socket, message) 74 | table.insert(socket_send_queue, message) 75 | end 76 | 77 | 78 | return M 79 | -------------------------------------------------------------------------------- /example/example-satori.script: -------------------------------------------------------------------------------- 1 | local nakama = require "nakama.nakama" 2 | local satori = require "satori.satori" 3 | local log = require "nakama.util.log" 4 | local retries = require "nakama.util.retries" 5 | local defold = require "nakama.engine.defold" 6 | local satori_session = require "nakama.session" 7 | local time = require "nakama.util.time" 8 | 9 | local function refresh_session(client, session) 10 | session = client.session_refresh(session.refresh_token) 11 | if session.token then 12 | satori_session.store(session, "satori") 13 | client.set_bearer_token(session.token) 14 | return true 15 | end 16 | log("Unable to refresh session") 17 | return false 18 | end 19 | 20 | local function login_uuid(client) 21 | local uuid = defold.uuid() 22 | local result = client.authenticate(nil, nil, uuid) 23 | if result.token then 24 | client.set_bearer_token(result.token) 25 | return true 26 | end 27 | log("Unable to login") 28 | return false 29 | end 30 | 31 | local function authenticate(client) 32 | -- restore a session 33 | local session = satori_session.restore("satori") 34 | 35 | local success = true 36 | 37 | if session and satori_session.is_token_expired_soon(session) and not satori_session.is_refresh_token_expired(session) then 38 | log("Session has expired or is about to expire. Refreshing.") 39 | success = refresh_session(client, session) 40 | elseif not session or satori_session.is_refresh_token_expired(session) then 41 | log("Session does not exist or it has expired. Must reauthenticate.") 42 | success = login_uuid(client) 43 | else 44 | client.set_bearer_token(session.token) 45 | end 46 | return success 47 | end 48 | 49 | 50 | local function run(self) 51 | log.print() 52 | 53 | local config = { 54 | host = "", 55 | use_ssl = true, 56 | port = 443, 57 | api_key = "", 58 | retry_policy = retries.incremental(5, 1), 59 | engine = defold, 60 | } 61 | local client = satori.create_client(config) 62 | 63 | satori.sync(function() 64 | log("authenticating") 65 | if not authenticate(client) then 66 | return 67 | end 68 | 69 | log("getting experiements") 70 | local experiments = satori.get_experiments(client) 71 | pprint(experiments) 72 | 73 | log("sending gameStarted event") 74 | local events_table = { 75 | satori.create_api_event(nil, nil, "gameStarted", time.now(), "my_value") 76 | } 77 | local result = satori.event(client, events_table) 78 | pprint(result) 79 | end) 80 | end 81 | 82 | function on_message(self, message_id, message, sender) 83 | if message_id == hash("run") then 84 | run(self) 85 | end 86 | end -------------------------------------------------------------------------------- /codegen/template-satori.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const MAIN_TEMPLATE string = `-- Code generated by codegen/generate-rest.go. DO NOT EDIT. 4 | 5 | --[[-- 6 | The Satori client SDK for Defold. 7 | 8 | @module satori 9 | ]] 10 | 11 | %%COMMON_TEMPLATE%% 12 | 13 | local _config = {} 14 | 15 | --- Create a Satori client instance. 16 | -- @param config A table of configuration options. 17 | -- config.engine - Engine specific implementations. 18 | -- config.host 19 | -- config.port 20 | -- config.timeout 21 | -- config.use_ssl - Use secure or non-secure sockets. 22 | -- config.bearer_token 23 | -- config.username 24 | -- config.password 25 | -- @return Satori Client instance. 26 | function M.create_client(config) 27 | assert(config, "You must provide a configuration") 28 | assert(config.host, "You must provide a host") 29 | assert(config.port, "You must provide a port") 30 | assert(config.api_key, "You must provide an api key") 31 | assert(config.engine, "You must provide an engine") 32 | assert(type(config.engine.http) == "function", "The engine must provide the 'http' function") 33 | assert(type(config.engine.socket_create) == "function", "The engine must provide the 'socket_create' function") 34 | assert(type(config.engine.socket_connect) == "function", "The engine must provide the 'socket_connect' function") 35 | assert(type(config.engine.socket_send) == "function", "The engine must provide the 'socket_send' function") 36 | log("init()") 37 | 38 | local client = {} 39 | local scheme = config.use_ssl and "https" or "http" 40 | client.engine = config.engine 41 | client.config = {} 42 | client.config.host = config.host 43 | client.config.port = config.port 44 | client.config.http_uri = ("%s://%s:%d"):format(scheme, config.host, config.port) 45 | client.config.bearer_token = config.bearer_token 46 | client.config.username = config.username or config.api_key 47 | client.config.password = config.password or "" 48 | client.config.timeout = config.timeout or 10 49 | client.config.use_ssl = config.use_ssl 50 | client.config.retry_policy = config.retry_policy or retries.none() 51 | 52 | local ignored_fns = { create_client = true, sync = true } 53 | for name,fn in pairs(M) do 54 | if not ignored_fns[name] and type(fn) == "function" then 55 | --log("setting " .. name) 56 | client[name] = function(...) return fn(client, ...) end 57 | end 58 | end 59 | 60 | return client 61 | end 62 | 63 | --- Set client bearer token. 64 | -- @param client Satori client. 65 | -- @param bearer_token Authorization bearer token. 66 | function M.set_bearer_token(client, bearer_token) 67 | assert(client, "You must provide a client") 68 | client.config.bearer_token = bearer_token 69 | end 70 | 71 | return M 72 | ` 73 | -------------------------------------------------------------------------------- /nakama/util/uri.lua: -------------------------------------------------------------------------------- 1 | -- The MIT License (MIT) 2 | -- Copyright (c) 2015-2021 Daurnimator 3 | -- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | -- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 5 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 6 | -- 7 | -- uri component encode/decode (https://github.com/daurnimator/lua-http/blob/master/http/util.lua) 8 | 9 | local M = {} 10 | 11 | -- Encodes a character as a percent encoded string 12 | local function char_to_pchar(c) 13 | return string.format("%%%02X", c:byte(1,1)) 14 | end 15 | 16 | -- replaces all characters except the following with the appropriate UTF-8 escape sequences: 17 | -- ; , / ? : @ & = + $ 18 | -- alphabetic, decimal digits, - _ . ! ~ * ' ( ) 19 | -- # 20 | function M.encode(str) 21 | return (str:gsub("[^%;%,%/%?%:%@%&%=%+%$%w%-%_%.%!%~%*%'%(%)%#]", char_to_pchar)) 22 | end 23 | 24 | -- escapes all characters except the following: alphabetic, decimal digits, - _ . ! ~ * ' ( ) 25 | function M.encode_component(str) 26 | return (str:gsub("[^%w%-_%.%!%~%*%'%(%)]", char_to_pchar)) 27 | end 28 | 29 | -- unescapes url encoded characters 30 | -- excluding characters that are special in urls 31 | local decodeURI_blacklist = {} 32 | for char in ("#$&+,/:;=?@"):gmatch(".") do 33 | decodeURI_blacklist[string.byte(char)] = true 34 | end 35 | local function decodeURI_helper(str) 36 | local x = tonumber(str, 16) 37 | if not decodeURI_blacklist[x] then 38 | return string.char(x) 39 | end 40 | -- return nothing; gsub will not perform the replacement 41 | end 42 | function M.decode(str) 43 | return (str:gsub("%%(%x%x)", decodeURI_helper)) 44 | end 45 | 46 | -- Converts a hex string to a character 47 | local function pchar_to_char(str) 48 | return string.char(tonumber(str, 16)) 49 | end 50 | 51 | function M.decode_component(str) 52 | return (str:gsub("%%(%x%x)", pchar_to_char)) 53 | end 54 | 55 | return M 56 | -------------------------------------------------------------------------------- /codegen/template-nakama.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const MAIN_TEMPLATE string = `-- Code generated by codegen/generate-rest.go. DO NOT EDIT. 4 | 5 | --[[-- 6 | The Nakama client SDK for Defold. 7 | 8 | @module nakama 9 | ]] 10 | 11 | %%COMMON_TEMPLATE%% 12 | 13 | local _config = {} 14 | 15 | --- Create a Nakama client instance. 16 | -- @param config A table of configuration options. 17 | -- config.engine - Engine specific implementations. 18 | -- config.host 19 | -- config.port 20 | -- config.timeout 21 | -- config.use_ssl - Use secure or non-secure sockets. 22 | -- config.bearer_token 23 | -- config.username 24 | -- config.password 25 | -- @return Nakama Client instance. 26 | function M.create_client(config) 27 | assert(config, "You must provide a configuration") 28 | assert(config.host, "You must provide a host") 29 | assert(config.port, "You must provide a port") 30 | assert(config.engine, "You must provide an engine") 31 | assert(type(config.engine.http) == "function", "The engine must provide the 'http' function") 32 | assert(type(config.engine.socket_create) == "function", "The engine must provide the 'socket_create' function") 33 | assert(type(config.engine.socket_connect) == "function", "The engine must provide the 'socket_connect' function") 34 | assert(type(config.engine.socket_send) == "function", "The engine must provide the 'socket_send' function") 35 | log("init()") 36 | 37 | local client = {} 38 | local scheme = config.use_ssl and "https" or "http" 39 | client.engine = config.engine 40 | client.config = {} 41 | client.config.host = config.host 42 | client.config.port = config.port 43 | client.config.http_uri = ("%s://%s:%d"):format(scheme, config.host, config.port) 44 | client.config.bearer_token = config.bearer_token 45 | client.config.username = config.username 46 | client.config.password = config.password 47 | client.config.timeout = config.timeout or 10 48 | client.config.use_ssl = config.use_ssl 49 | client.config.retry_policy = config.retry_policy or retries.none() 50 | 51 | local ignored_fns = { create_client = true, sync = true } 52 | for name,fn in pairs(M) do 53 | if not ignored_fns[name] and type(fn) == "function" then 54 | --log("setting " .. name) 55 | client[name] = function(...) return fn(client, ...) end 56 | end 57 | end 58 | 59 | return client 60 | end 61 | 62 | 63 | --- Create a Nakama socket. 64 | -- @param client The client to create the socket for. 65 | -- @return Socket instance. 66 | function M.create_socket(client) 67 | assert(client, "You must provide a client") 68 | return socket.create(client) 69 | end 70 | 71 | --- Set Nakama client bearer token. 72 | -- @param client Nakama client. 73 | -- @param bearer_token Authorization bearer token. 74 | function M.set_bearer_token(client, bearer_token) 75 | assert(client, "You must provide a client") 76 | client.config.bearer_token = bearer_token 77 | end 78 | 79 | 80 | 81 | return M 82 | ` 83 | -------------------------------------------------------------------------------- /docs/modules/nakama.util.async.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | SDK API documentation 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 52 | 53 |
54 | 55 |

Module nakama.util.async

56 |

Run functions asynchronously.

57 |

58 | 59 | 60 |

Functions

61 | 62 | 63 | 64 | 65 | 66 |
async (fn, ...)Execute a function asynchronously as a coroutines and return the result.
67 | 68 |
69 |
70 | 71 | 72 |

Functions

73 | 74 |
75 |
76 | 77 | async (fn, ...) 78 |
79 |
80 | Execute a function asynchronously as a coroutines and return the result. 81 | 82 | 83 |

Parameters:

84 |
    85 |
  • fn 86 | The function to execute. 87 |
  • 88 |
  • ... 89 | Function params. 90 |
  • 91 |
92 | 93 |

Returns:

94 |
    95 | 96 | The result of executing the function. 97 |
98 | 99 | 100 | 101 | 102 |
103 |
104 | 105 | 106 |
107 |
108 |
109 | generated by LDoc 1.4.6 110 | Last updated 2021-11-05 15:42:41 111 |
112 |
113 | 114 | 115 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | SDK API documentation 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 45 | 46 |
47 | 48 | 49 |

Nakama client SDK for Defold

50 |

Developed by The Defold Foundation and maintained by Heroic Labs.

51 | 52 |

Modules

53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 |
nakama.engine.defoldNakama defold integration.
nakamaThe Nakama client SDK for Defold.
nakama.sessionCreate and check Nakama sessions.
nakama.util.asyncRun functions asynchronously.
nakama.util.base64Base64 encode and decode data.
nakama.util.jsonJSON encode and decode data.
nakama.util.logNakama logging module.
nakama.util.uuidWork with universally unique identifiers (UUIDs).
87 | 88 |
89 |
90 |
91 | generated by LDoc 1.4.6 92 | Last updated 2021-11-05 15:42:41 93 |
94 |
95 | 96 | 97 | -------------------------------------------------------------------------------- /example/example-nakama.script: -------------------------------------------------------------------------------- 1 | local nakama = require "nakama.nakama" 2 | local log = require "nakama.util.log" 3 | local retries = require "nakama.util.retries" 4 | local defold = require "nakama.engine.defold" 5 | local nakama_session = require "nakama.session" 6 | 7 | 8 | local function email_login(client, email, password, username) 9 | local session = client.authenticate_email(email, password, nil, true, username) 10 | if session and session.token then 11 | nakama_session.store(session) 12 | client.set_bearer_token(session.token) 13 | return true 14 | end 15 | log("Unable to login") 16 | return false 17 | end 18 | 19 | local function device_login(client) 20 | local uuid = defold.uuid() 21 | local result = client.authenticate_device(uuid, nil, true) 22 | if result.token then 23 | client.set_bearer_token(result.token) 24 | return true 25 | end 26 | local result = client.authenticate_device(uuid, nil, false) 27 | if result.token then 28 | client.set_bearer_token(result.token) 29 | return true 30 | end 31 | log("Unable to login") 32 | return false 33 | end 34 | 35 | local function refresh_session(client, session) 36 | session = client.session_refresh(session.refresh_token) 37 | if session.token then 38 | nakama_session.store(session) 39 | client.set_bearer_token(session.token) 40 | return true 41 | end 42 | log("Unable to refresh session") 43 | return false 44 | end 45 | 46 | local function login(client) 47 | -- restore a session 48 | local session = nakama_session.restore() 49 | 50 | local success = true 51 | 52 | if session and nakama_session.is_token_expired_soon(session) and not nakama_session.is_refresh_token_expired(session) then 53 | log("Session has expired or is about to expire. Refreshing.") 54 | success = refresh_session(client, session) 55 | elseif not session or nakama_session.is_refresh_token_expired(session) then 56 | log("Session does not exist or it has expired. Must reauthenticate.") 57 | success = email_login(client, "bjorn@defold.se", "foobar123", "britzl") 58 | else 59 | client.set_bearer_token(session.token) 60 | end 61 | return success 62 | end 63 | 64 | local function run(self) 65 | log.print() 66 | 67 | local config = { 68 | host = "127.0.0.1", 69 | port = 7350, 70 | username = "defaultkey", 71 | password = "", 72 | retry_policy = retries.incremental(5, 1), 73 | engine = defold, 74 | } 75 | local client = nakama.create_client(config) 76 | 77 | nakama.sync(function() 78 | 79 | local ok = login(client) 80 | if not ok then 81 | return 82 | end 83 | 84 | local account = client.get_account() 85 | pprint(account) 86 | 87 | local socket = client.create_socket() 88 | socket.on_channel_message(function(message) 89 | pprint(message) 90 | end) 91 | socket.on_channel_presence_event(function(message) 92 | pprint(message) 93 | end) 94 | local ok, err = socket.connect() 95 | if not ok then 96 | log("Unable to connect: ", err) 97 | return 98 | end 99 | 100 | local channel_id = "pineapple-pizza-lovers-room" 101 | local target = channel_id 102 | local type = 1 -- 1 = room, 2 = Direct Message, 3 = Group 103 | local persistence = false 104 | local hidden = false 105 | local result = socket.channel_join(target, type, persistence, hidden) 106 | pprint(result) 107 | end) 108 | end 109 | 110 | function on_message(self, message_id, message, sender) 111 | if message_id == hash("run") then 112 | run(self) 113 | end 114 | end -------------------------------------------------------------------------------- /docs/modules/nakama.util.log.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | SDK API documentation 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 52 | 53 |
54 | 55 |

Module nakama.util.log

56 |

Nakama logging module.

57 |

58 | 59 | 60 |

Functions

61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
silent ()Silence all logging.
print ()Print all log messages to the default system output.
custom (fn)Set a custom log function.
75 | 76 |
77 |
78 | 79 | 80 |

Functions

81 | 82 |
83 |
84 | 85 | silent () 86 |
87 |
88 | Silence all logging. 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 |
97 |
98 | 99 | print () 100 |
101 |
102 | Print all log messages to the default system output. 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 |
111 |
112 | 113 | custom (fn) 114 |
115 |
116 | Set a custom log function. 117 | 118 | 119 |

Parameters:

120 |
    121 |
  • fn 122 | The custom log function. 123 |
  • 124 |
125 | 126 | 127 | 128 | 129 | 130 |
131 |
132 | 133 | 134 |
135 |
136 |
137 | generated by LDoc 1.4.6 138 | Last updated 2021-11-05 15:42:41 139 |
140 |
141 | 142 | 143 | -------------------------------------------------------------------------------- /docs/modules/nakama.util.base64.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | SDK API documentation 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 52 | 53 |
54 | 55 |

Module nakama.util.base64

56 |

Base64 encode and decode data.

57 |

58 |

From http://lua-users.org/wiki/BaseSixtyFour 59 |

60 | 61 | 62 |

Functions

63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 |
encode (data)Base64 encode a data string.
decode (data)Decode a base64 encoded data string.
73 | 74 |
75 |
76 | 77 | 78 |

Functions

79 | 80 |
81 |
82 | 83 | encode (data) 84 |
85 |
86 | Base64 encode a data string. 87 | 88 | 89 |

Parameters:

90 |
    91 |
  • data 92 | The data string to encode. 93 |
  • 94 |
95 | 96 |

Returns:

97 |
    98 | 99 | The encoded data string. 100 |
101 | 102 | 103 | 104 | 105 |
106 |
107 | 108 | decode (data) 109 |
110 |
111 | Decode a base64 encoded data string. 112 | 113 | 114 |

Parameters:

115 |
    116 |
  • data 117 | The encoded base64 string to decode. 118 |
  • 119 |
120 | 121 |

Returns:

122 |
    123 | 124 | The decoded data string. 125 |
126 | 127 | 128 | 129 | 130 |
131 |
132 | 133 | 134 |
135 |
136 |
137 | generated by LDoc 1.4.6 138 | Last updated 2021-11-05 15:42:41 139 |
140 |
141 | 142 | 143 | -------------------------------------------------------------------------------- /docs/modules/nakama.session.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | SDK API documentation 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 52 | 53 |
54 | 55 |

Module nakama.session

56 |

Create and check Nakama sessions.

57 |

58 | 59 | 60 |

Functions

61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
expired (session)Check whether a Nakama session has expired or not.
create (data)Create a session object with the given data and included token.
71 | 72 |
73 |
74 | 75 | 76 |

Functions

77 | 78 |
79 |
80 | 81 | expired (session) 82 |
83 |
84 | Check whether a Nakama session has expired or not. 85 | 86 | 87 |

Parameters:

88 |
    89 |
  • session 90 | The session object created with session.create. 91 |
  • 92 |
93 | 94 |

Returns:

95 |
    96 | 97 | A boolean if the session has expired or not. 98 |
99 | 100 | 101 | 102 | 103 |
104 |
105 | 106 | create (data) 107 |
108 |
109 | Create a session object with the given data and included token. 110 | 111 | 112 |

Parameters:

113 |
    114 |
  • data 115 | A data table containing a "token" attribute. 116 |
  • 117 |
118 | 119 |

Returns:

120 |
    121 | 122 | The session object. 123 |
124 | 125 | 126 | 127 | 128 |
129 |
130 | 131 | 132 |
133 |
134 |
135 | generated by LDoc 1.4.6 136 | Last updated 2021-11-05 15:42:41 137 |
138 |
139 | 140 | 141 | -------------------------------------------------------------------------------- /nakama/session.lua: -------------------------------------------------------------------------------- 1 | --[[-- 2 | Create and check sessions. 3 | 4 | @module nakama.util.session 5 | ]] 6 | 7 | 8 | local b64 = require "nakama.util.b64" 9 | local json = require "nakama.util.json" 10 | local log = require "nakama.util.log" 11 | 12 | local M = {} 13 | 14 | local JWT_TOKEN = "^(.-)%.(.-)%.(.-)$" 15 | 16 | local TWENTYFOUR_HOURS = 60 * 60 * 24 17 | 18 | --- Check whether a Nakama session token is about to expire (within 24 hours) 19 | -- @param session The session object created with session.create(). 20 | -- @return A boolean if the token is about to expire or not. 21 | function M.is_token_expired_soon(session) 22 | assert(session, "You must provide a session") 23 | return os.time() + TWENTYFOUR_HOURS > session.expires 24 | end 25 | 26 | --- Check whether a Nakama session token has expired or not. 27 | -- @param session The session object created with session.create(). 28 | -- @return A boolean if the token has expired or not. 29 | function M.is_token_expired(session) 30 | assert(session, "You must provide a session") 31 | return os.time() > session.expires 32 | end 33 | -- for backwards compatibility 34 | function M.expired(session) 35 | return M.is_token_expired(session) 36 | end 37 | 38 | --- Check whether a Nakama session refresh token has expired or not. 39 | -- @param session The session object created with session.create(). 40 | -- @return A boolean if the refresh token has expired or not. 41 | function M.is_refresh_token_expired(session) 42 | assert(session, "You must provide a session") 43 | if not session.refresh_token_expires then 44 | return true 45 | end 46 | return os.time() > session.refresh_token_expires 47 | end 48 | 49 | --- Decode JWT token 50 | -- @param token base 64 encoded JWT token 51 | -- @return decoded token table 52 | local function decode_token(token) 53 | local p1, p2, p3 = token:match(JWT_TOKEN) 54 | assert(p1 and p2 and p3, "jwt is not valid") 55 | return json.decode(b64.decode(p2)) 56 | end 57 | 58 | --- Create a session object with the given data and included token. 59 | -- @param data A data table containing a "token", "refresh_token" and other additional information. 60 | -- @return The session object. 61 | function M.create(data) 62 | assert(data.token, "You must provide a token") 63 | 64 | local session = { 65 | created = os.time() 66 | } 67 | 68 | local decoded_token = decode_token(data.token) 69 | session.token = data.token 70 | session.expires = decoded_token.exp 71 | session.username = decoded_token.usn 72 | session.user_id = decoded_token.uid 73 | session.vars = decoded_token.vrs 74 | 75 | if data.refresh_token then 76 | local decoded_refresh_token = decode_token(data.refresh_token) 77 | session.refresh_token = data.refresh_token 78 | session.refresh_token_expires = decoded_refresh_token.exp 79 | session.refresh_token_username = decoded_refresh_token.usn 80 | session.refresh_token_user_id = decoded_refresh_token.uid 81 | session.refresh_token_vars = decoded_refresh_token.vrs 82 | end 83 | return session 84 | end 85 | 86 | 87 | local function get_session_save_filename(id) 88 | local project_tite = sys.get_config("project.title") 89 | local application_id = b64.encode(project_tite) 90 | return sys.get_save_file(application_id, id .. ".session") 91 | end 92 | 93 | --- Store a session on disk 94 | -- @param session The session to store 95 | -- @param id Id of the session (optional, defaults to "nakama") 96 | -- @return sucess 97 | function M.store(session, id) 98 | assert(session) 99 | local filename = get_session_save_filename(id or "nakama") 100 | return sys.save(filename, session) 101 | end 102 | 103 | --- Restore a session previously stored using session.store() 104 | -- @param id Id of the session (optional, defaults to "nakama") 105 | -- @return The session or nil if no session has been stored 106 | function M.restore(id) 107 | local filename = get_session_save_filename(id or "nakama") 108 | local session = sys.load(filename) 109 | if not session.token then 110 | return nil 111 | end 112 | return session 113 | end 114 | 115 | return M 116 | -------------------------------------------------------------------------------- /test/test_nakama.lua: -------------------------------------------------------------------------------- 1 | local nakama = require "nakama.nakama" 2 | local test_engine = require "nakama.engine.test" 3 | local json = require "nakama.util.json" 4 | local log = require "nakama.util.log" 5 | log.print() 6 | 7 | context("Nakama client", function() 8 | 9 | before(function() 10 | test_engine.reset() 11 | end) 12 | after(function() end) 13 | 14 | local function config() 15 | return { 16 | host = "127.0.0.1", 17 | port = 7350, 18 | use_ssl = false, 19 | username = "defaultkey", 20 | password = "", 21 | engine = test_engine, 22 | timeout = 10, -- connection timeout in seconds 23 | } 24 | end 25 | 26 | local function pprint(t) 27 | for k,v in pairs(t) do 28 | print(k, v) 29 | end 30 | end 31 | 32 | test("It should be able to create a client", function() 33 | local config = config() 34 | local client = nakama.create_client(config) 35 | assert_not_nil(client) 36 | end) 37 | 38 | test("It should be able to authenticate", function() 39 | local token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiI1MjJkMGI5MS00NmQzLTRjY2ItYmIwYS0wNTFjYjUyOGNhMDMiLCJ1c24iOiJicml0emwiLCJleHAiOjE2NjE1OTA5Nzl9.r3h4QraXsXl-XmGQueYecjeb6223vtd1s-Ak1K_FrGM" 40 | local data = { token = token } 41 | local url_path = "/v2/account/authenticate/email" 42 | test_engine.set_http_response(url_path, data) 43 | 44 | coroutine.wrap(function() 45 | local client = nakama.create_client(config()) 46 | local email = "super@heroes.com" 47 | local password = "batsignal" 48 | client.authenticate_email(email, password) 49 | 50 | local request = test_engine.get_http_request(1) 51 | assert_not_nil(request) 52 | assert_equal(request.url_path, url_path) 53 | assert_equal(request.method, "POST") 54 | 55 | local pd = json.decode(request.post_data) 56 | assert_equal(pd.password, password) 57 | assert_equal(pd.email, email) 58 | assert_not_nil(request.query_params) 59 | end)() 60 | end) 61 | 62 | test("It should create a session on successful authentication", function() 63 | local token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiI1MjJkMGI5MS00NmQzLTRjY2ItYmIwYS0wNTFjYjUyOGNhMDMiLCJ1c24iOiJicml0emwiLCJleHAiOjE2NjE1OTA5Nzl9.r3h4QraXsXl-XmGQueYecjeb6223vtd1s-Ak1K_FrGM" 64 | local refresh_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiI1MjJkMGI5MS00NmQzLTRjY2ItYmIwYS0wNTFjYjUyOGNhMDMiLCJ1c24iOiJicml0emwiLCJleHAiOjE2NjE1ODczNzl9.AWASctuZx9A8YliCLSj9jtOi4fuXUZaWtRdNz1mMEEw" 65 | local data = { 66 | token = token, 67 | } 68 | local url_path = "/v2/account/authenticate/email" 69 | test_engine.set_http_response(url_path, data) 70 | 71 | coroutine.wrap(function() 72 | local client = nakama.create_client(config()) 73 | local email = "super@heroes.com" 74 | local password = "batsignal" 75 | local session = client.authenticate_email(email, password) 76 | assert_not_nil(session) 77 | assert_not_nil(session.created) 78 | assert_not_nil(session.expires) 79 | assert_equal(session.token, data.token) 80 | assert_equal(session.user_id, "522d0b91-46d3-4ccb-bb0a-051cb528ca03") 81 | assert_equal(session.username, "britzl") 82 | end)() 83 | end) 84 | 85 | test("It should be able to use coroutines", function() 86 | test_engine.set_http_response("/v2/account", {}) 87 | 88 | local done = false 89 | print("before coroutine") 90 | coroutine.wrap(function() 91 | print("in coroutine create client") 92 | local client = nakama.create_client(config()) 93 | print("in coroutine get account") 94 | local result = client.get_account() 95 | print("in coroutine result", result) 96 | assert_not_nil(result) 97 | print("in coroutine done") 98 | done = true 99 | end)() 100 | print("after coroutine") 101 | assert_true(done) 102 | end) 103 | 104 | test("It should be able to use callbacks", function() 105 | test_engine.set_http_response("/v2/account", {}) 106 | 107 | local done = false 108 | local client = nakama.create_client(config()) 109 | client.get_account(function(result) 110 | assert_not_nil(result) 111 | done = true 112 | end) 113 | assert_true(done) 114 | end) 115 | end) 116 | 117 | 118 | -------------------------------------------------------------------------------- /test/test_satori.lua: -------------------------------------------------------------------------------- 1 | local satori = require "satori.satori" 2 | local test_engine = require "nakama.engine.test" 3 | local json = require "nakama.util.json" 4 | local log = require "nakama.util.log" 5 | log.print() 6 | 7 | context("Satori client", function() 8 | 9 | before(function() 10 | test_engine.reset() 11 | end) 12 | after(function() end) 13 | 14 | local function config() 15 | return { 16 | host = "127.0.0.1", 17 | api_key = "00000000-1111-2222-3333-444444444444", 18 | port = 7350, 19 | use_ssl = false, 20 | engine = test_engine, 21 | timeout = 10, -- connection timeout in seconds 22 | } 23 | end 24 | 25 | local function pprint(...) 26 | for i=1,select("#", ...) do 27 | local arg = select(i, ...) 28 | if type(arg) == "table" then 29 | for k,v in pairs(arg) do 30 | print(k, v) 31 | end 32 | else 33 | print(arg) 34 | end 35 | end 36 | end 37 | 38 | test("It should be able to create a client", function() 39 | local config = config() 40 | local client = satori.create_client(config) 41 | assert_not_nil(client) 42 | end) 43 | 44 | test("It should be able to authenticate", function() 45 | local token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiI1MjJkMGI5MS00NmQzLTRjY2ItYmIwYS0wNTFjYjUyOGNhMDMiLCJ1c24iOiJicml0emwiLCJleHAiOjE2NjE1OTA5Nzl9.r3h4QraXsXl-XmGQueYecjeb6223vtd1s-Ak1K_FrGM" 46 | local data = { token = token } 47 | local url_path = "/v1/authenticate" 48 | test_engine.set_http_response(url_path, data) 49 | 50 | coroutine.wrap(function() 51 | local client = satori.create_client(config()) 52 | local id = "foobar" 53 | client.authenticate(nil, nil, id) 54 | 55 | local request = test_engine.get_http_request(1) 56 | assert_not_nil(request) 57 | assert_equal(request.url_path, url_path) 58 | assert_equal(request.method, "POST") 59 | 60 | local pd = json.decode(request.post_data) 61 | assert_equal(pd.id, id) 62 | assert_not_nil(request.query_params) 63 | end)() 64 | end) 65 | 66 | test("It should create a session on successful authentication", function() 67 | local token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaWQiOiI5NzM3Zjc2My1kZDNkLTQ4OWMtYTI0Yy1hOTUwY2E1OWI5M2QiLCJpaWQiOiI3NDkzNDc3MS0zOTQ3LTQ2YjQtYzY5Zi0wYTc2ODAxMGYxOTciLCJleHAiOjE3MTc3NDg1NzMsImlhdCI6MTcxNzc0NDk3MywiYXBpIjoiZGVmb2xkIn0.v0Mnf-b1g738PWPSf-EsHqH1I6BpZ9QErmHU6t-SPpQ" 68 | local refresh_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaWQiOiI5NzM3Zjc2My1kZDNkLTQ4OWMtYTI0Yy1hOTUwY2E1OWI5M2QiLCJpaWQiOiI3NDkzNDc3MS0zOTQ3LTQ2YjQtYzY5Zi0wYTc2ODAxMGYxOTciLCJleHAiOjE3MTc3NDg1NzMsImlhdCI6MTcxNzc0NDk3M30.IK9xAbIxSv68awVw8yiaYnTun_Dd-VJWjS3rld10NMI" 69 | local data = { 70 | token = token, 71 | } 72 | local url_path = "/v1/authenticate" 73 | test_engine.set_http_response(url_path, data) 74 | 75 | coroutine.wrap(function() 76 | local client = satori.create_client(config()) 77 | local id = "foobar" 78 | local session = client.authenticate(nil, nil, id) 79 | assert_not_nil(session) 80 | assert_not_nil(session.created) 81 | assert_not_nil(session.expires) 82 | assert_equal(session.token, data.token) 83 | end)() 84 | end) 85 | 86 | test("It should be able to use coroutines", function() 87 | test_engine.set_http_response("/v1/experiment", {}) 88 | 89 | local done = false 90 | print("before coroutine") 91 | coroutine.wrap(function() 92 | print("in coroutine create client") 93 | local client = satori.create_client(config()) 94 | print("in coroutine get account") 95 | local result = client.get_experiments() 96 | print("in coroutine result", result) 97 | assert_not_nil(result) 98 | print("in coroutine done") 99 | done = true 100 | end)() 101 | print("after coroutine") 102 | assert_true(done) 103 | end) 104 | 105 | test("It should be able to use callbacks", function() 106 | test_engine.set_http_response("/v1/experiment", {}) 107 | 108 | local done = false 109 | local client = satori.create_client(config()) 110 | client.get_experiments(nil, function(result) 111 | assert_not_nil(result) 112 | done = true 113 | end) 114 | assert_true(done) 115 | end) 116 | end) 117 | -------------------------------------------------------------------------------- /test/test_socket.lua: -------------------------------------------------------------------------------- 1 | local nakama = require "nakama.nakama" 2 | local test_engine = require "nakama.engine.test" 3 | local b64 = require "nakama.util.b64" 4 | 5 | 6 | context("Nakama socket", function() 7 | 8 | before(function() 9 | test_engine.reset() 10 | end) 11 | after(function() end) 12 | 13 | local function config() 14 | return { 15 | host = "127.0.0.1", 16 | port = 7350, 17 | use_ssl = false, 18 | username = "defaultkey", 19 | password = "", 20 | engine = test_engine, 21 | timeout = 10, -- connection timeout in seconds 22 | } 23 | end 24 | 25 | local function pprint(t) 26 | for k,v in pairs(t) do 27 | print(k, v) 28 | end 29 | end 30 | 31 | test("It should be able to create a socket", function() 32 | local client = nakama.create_client(config()) 33 | local socket = client.create_socket() 34 | assert_not_nil(socket) 35 | end) 36 | 37 | test("It should be able to connect from a coroutine", function() 38 | local client = nakama.create_client(config()) 39 | local socket = client.create_socket() 40 | 41 | local done = false 42 | coroutine.wrap(function() 43 | local result = socket.connect() 44 | assert_true(result) 45 | done = true 46 | end)() 47 | assert_true(done) 48 | end) 49 | 50 | test("It should be able to connect from a callback", function() 51 | local client = nakama.create_client(config()) 52 | local socket = client.create_socket() 53 | 54 | local done = false 55 | socket.connect(function(result) 56 | assert_true(result) 57 | done = true 58 | end) 59 | assert_true(done) 60 | end) 61 | 62 | test("It should be able to disconnect", function() 63 | local client = nakama.create_client(config()) 64 | local socket = client.create_socket() 65 | 66 | socket.disconnect() 67 | assert_nil(socket.connection) 68 | end) 69 | 70 | test("It should encode sent match data", function() 71 | local client = nakama.create_client(config()) 72 | local socket = client.create_socket() 73 | 74 | local done = false 75 | coroutine.wrap(function() 76 | socket.connect() 77 | local match_id = "id1234" 78 | local op_code = 1 79 | local data = "somedata" 80 | socket.match_data_send(match_id, op_code, data) 81 | 82 | local message = test_engine.get_socket_message() 83 | assert_not_nil(message) 84 | assert_not_nil(message.match_data_send) 85 | assert_equal(message.match_data_send.op_code, op_code) 86 | assert_equal(message.match_data_send.match_id, match_id) 87 | assert_equal(message.match_data_send.data, b64.encode(data)) 88 | done = true 89 | end)() 90 | assert_true(done) 91 | end) 92 | 93 | test("It should decode received match data", function() 94 | local client = nakama.create_client(config()) 95 | local socket = client.create_socket() 96 | 97 | local done = false 98 | coroutine.wrap(function() 99 | local data = "somedata" 100 | local message = { 101 | match_data = { 102 | data = b64.encode(data) 103 | } 104 | } 105 | 106 | socket.connect() 107 | socket.on_match_data(function(message) 108 | assert_not_nil(message) 109 | assert_not_nil(message.match_data) 110 | assert_not_nil(message.match_data.data) 111 | assert_equal(message.match_data.data, data, "Expected decoded match data") 112 | done = true 113 | end) 114 | 115 | test_engine.receive_socket_message(socket, message) 116 | 117 | end)() 118 | assert_true(done) 119 | end) 120 | 121 | test("It should send socket events to listeners", function() 122 | local client = nakama.create_client(config()) 123 | local socket = client.create_socket() 124 | 125 | local events = { "notifications", "party_data", "stream_data" } 126 | local count = 0 127 | coroutine.wrap(function() 128 | socket.connect() 129 | 130 | for _,event_id in ipairs(events) do 131 | -- create event listener 132 | socket["on_" .. event_id](function(message) 133 | assert_not_nil(message) 134 | count = count + 1 135 | end) 136 | -- send message 137 | test_engine.receive_socket_message(socket, { [event_id] = {} }) 138 | end 139 | end)() 140 | assert_equal(count, #events, "Expected all events to be received") 141 | end) 142 | end) 143 | 144 | 145 | -------------------------------------------------------------------------------- /docs/modules/nakama.util.json.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | SDK API documentation 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 52 | 53 |
54 | 55 |

Module nakama.util.json

56 |

JSON encode and decode data.

57 |

58 |

Copyright (c) 2019 rxi 59 |

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 |

75 | 76 | 77 |

Functions

78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 |
encode (val)JSON encode data.
decode (str)Decode a JSON string and return the Lua data.
88 | 89 |
90 |
91 | 92 | 93 |

Functions

94 | 95 |
96 |
97 | 98 | encode (val) 99 |
100 |
101 | JSON encode data. 102 | 103 | 104 |

Parameters:

105 |
    106 |
  • val 107 | The Lua data to encode. 108 |
  • 109 |
110 | 111 |

Returns:

112 |
    113 | 114 | The JSON encoded result string. 115 |
116 | 117 | 118 | 119 | 120 |
121 |
122 | 123 | decode (str) 124 |
125 |
126 | Decode a JSON string and return the Lua data. 127 | 128 | 129 |

Parameters:

130 |
    131 |
  • str 132 | The encoded JSON string to decode. 133 |
  • 134 |
135 | 136 |

Returns:

137 |
    138 | 139 | The decoded Lua data. 140 |
141 | 142 | 143 | 144 | 145 |
146 |
147 | 148 | 149 |
150 |
151 |
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 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 52 | 53 |
54 | 55 |

Module nakama.engine.defold

56 |

Nakama defold integration.

57 |

58 | 59 | 60 |

Functions

61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 |
uuid ()Returns a UUID from the device's mac address.
http (config, url_path, query_params, method, post_data, callback)Make a HTTP request.
socket_create (config, on_message)Create a new socket with message handler.
socket_connect (socket, callback)Connect a created socket using web sockets.
socket_send (socket, message, callback)Send a socket message.
83 | 84 |
85 |
86 | 87 | 88 |

Functions

89 | 90 |
91 |
92 | 93 | uuid () 94 |
95 |
96 | Returns a UUID from the device's mac address. 97 | 98 | 99 | 100 |

Returns:

101 |
    102 | 103 | The UUID string. 104 |
105 | 106 | 107 | 108 | 109 |
110 |
111 | 112 | http (config, url_path, query_params, method, post_data, callback) 113 |
114 |
115 | Make a HTTP request. 116 | 117 | 118 |

Parameters:

119 |
    120 |
  • config 121 | The http config table, see Defold docs. 122 |
  • 123 |
  • url_path 124 | The request URL. 125 |
  • 126 |
  • query_params 127 | Query params string. 128 |
  • 129 |
  • method 130 | The HTTP method string. 131 |
  • 132 |
  • post_data 133 | String of post data. 134 |
  • 135 |
  • callback 136 | The callback function. 137 |
  • 138 |
139 | 140 |

Returns:

141 |
    142 | 143 | The mac address string. 144 |
145 | 146 | 147 | 148 | 149 |
150 |
151 | 152 | socket_create (config, on_message) 153 |
154 |
155 | Create a new socket with message handler. 156 | 157 | 158 |

Parameters:

159 |
    160 |
  • config 161 | The socket config table, see Defold docs. 162 |
  • 163 |
  • on_message 164 | Your function to process socket messages. 165 |
  • 166 |
167 | 168 |

Returns:

169 |
    170 | 171 | A socket table. 172 |
173 | 174 | 175 | 176 | 177 |
178 |
179 | 180 | socket_connect (socket, callback) 181 |
182 |
183 | Connect a created socket using web sockets. 184 | 185 | 186 |

Parameters:

187 |
    188 |
  • socket 189 | The socket table, see socket_create. 190 |
  • 191 |
  • callback 192 | The callback function. 193 |
  • 194 |
195 | 196 | 197 | 198 | 199 | 200 |
201 |
202 | 203 | socket_send (socket, message, callback) 204 |
205 |
206 | Send a socket message. 207 | 208 | 209 |

Parameters:

210 |
    211 |
  • socket 212 | The socket table, see socket_create. 213 |
  • 214 |
  • message 215 | The message string to send. 216 |
  • 217 |
  • callback 218 | The callback function. 219 |
  • 220 |
221 | 222 | 223 | 224 | 225 | 226 |
227 |
228 | 229 | 230 |
231 |
232 |
233 | generated by LDoc 1.4.6 234 | Last updated 2021-11-05 15:42:41 235 |
236 |
237 | 238 | 239 | -------------------------------------------------------------------------------- /docs/ldoc.css: -------------------------------------------------------------------------------- 1 | /* BEGIN RESET 2 | 3 | Copyright (c) 2010, Yahoo! Inc. All rights reserved. 4 | Code licensed under the BSD License: 5 | http://developer.yahoo.com/yui/license.html 6 | version: 2.8.2r1 7 | */ 8 | html { 9 | color: #000; 10 | background: #FFF; 11 | } 12 | body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td { 13 | margin: 0; 14 | padding: 0; 15 | } 16 | table { 17 | border-collapse: collapse; 18 | border-spacing: 0; 19 | } 20 | fieldset,img { 21 | border: 0; 22 | } 23 | address,caption,cite,code,dfn,em,strong,th,var,optgroup { 24 | font-style: inherit; 25 | font-weight: inherit; 26 | } 27 | del,ins { 28 | text-decoration: none; 29 | } 30 | li { 31 | margin-left: 20px; 32 | } 33 | caption,th { 34 | text-align: left; 35 | } 36 | h1,h2,h3,h4,h5,h6 { 37 | font-size: 100%; 38 | font-weight: bold; 39 | } 40 | q:before,q:after { 41 | content: ''; 42 | } 43 | abbr,acronym { 44 | border: 0; 45 | font-variant: normal; 46 | } 47 | sup { 48 | vertical-align: baseline; 49 | } 50 | sub { 51 | vertical-align: baseline; 52 | } 53 | legend { 54 | color: #000; 55 | } 56 | input,button,textarea,select,optgroup,option { 57 | font-family: inherit; 58 | font-size: inherit; 59 | font-style: inherit; 60 | font-weight: inherit; 61 | } 62 | input,button,textarea,select {*font-size:100%; 63 | } 64 | /* END RESET */ 65 | 66 | body { 67 | margin-left: 1em; 68 | margin-right: 1em; 69 | font-family: arial, helvetica, geneva, sans-serif; 70 | background-color: #ffffff; margin: 0px; 71 | } 72 | 73 | code, tt { font-family: monospace; font-size: 1.1em; } 74 | span.parameter { font-family:monospace; } 75 | span.parameter:after { content:":"; } 76 | span.types:before { content:"("; } 77 | span.types:after { content:")"; } 78 | .type { font-weight: bold; font-style:italic } 79 | 80 | body, p, td, th { font-size: .95em; line-height: 1.2em;} 81 | 82 | p, ul { margin: 10px 0 0 0px;} 83 | 84 | strong { font-weight: bold;} 85 | 86 | em { font-style: italic;} 87 | 88 | h1 { 89 | font-size: 1.5em; 90 | margin: 20px 0 20px 0; 91 | } 92 | h2, h3, h4 { margin: 15px 0 10px 0; } 93 | h2 { font-size: 1.25em; } 94 | h3 { font-size: 1.15em; } 95 | h4 { font-size: 1.06em; } 96 | 97 | a:link { font-weight: bold; color: #004080; text-decoration: none; } 98 | a:visited { font-weight: bold; color: #006699; text-decoration: none; } 99 | a:link:hover { text-decoration: underline; } 100 | 101 | hr { 102 | color:#cccccc; 103 | background: #00007f; 104 | height: 1px; 105 | } 106 | 107 | blockquote { margin-left: 3em; } 108 | 109 | ul { list-style-type: disc; } 110 | 111 | p.name { 112 | font-family: "Andale Mono", monospace; 113 | padding-top: 1em; 114 | } 115 | 116 | pre { 117 | background-color: rgb(245, 245, 245); 118 | border: 1px solid #C0C0C0; /* silver */ 119 | padding: 10px; 120 | margin: 10px 0 10px 0; 121 | overflow: auto; 122 | font-family: "Andale Mono", monospace; 123 | } 124 | 125 | pre.example { 126 | font-size: .85em; 127 | } 128 | 129 | table.index { border: 1px #00007f; } 130 | table.index td { text-align: left; vertical-align: top; } 131 | 132 | #container { 133 | margin-left: 1em; 134 | margin-right: 1em; 135 | background-color: #f0f0f0; 136 | } 137 | 138 | #product { 139 | text-align: center; 140 | border-bottom: 1px solid #cccccc; 141 | background-color: #ffffff; 142 | } 143 | 144 | #product big { 145 | font-size: 2em; 146 | } 147 | 148 | #main { 149 | background-color: #f0f0f0; 150 | border-left: 2px solid #cccccc; 151 | } 152 | 153 | #navigation { 154 | float: left; 155 | width: 14em; 156 | vertical-align: top; 157 | background-color: #f0f0f0; 158 | overflow: visible; 159 | } 160 | 161 | #navigation h2 { 162 | background-color:#e7e7e7; 163 | font-size:1.1em; 164 | color:#000000; 165 | text-align: left; 166 | padding:0.2em; 167 | border-top:1px solid #dddddd; 168 | border-bottom:1px solid #dddddd; 169 | } 170 | 171 | #navigation ul 172 | { 173 | font-size:1em; 174 | list-style-type: none; 175 | margin: 1px 1px 10px 1px; 176 | } 177 | 178 | #navigation li { 179 | text-indent: -1em; 180 | display: block; 181 | margin: 3px 0px 0px 22px; 182 | } 183 | 184 | #navigation li li a { 185 | margin: 0px 3px 0px -1em; 186 | } 187 | 188 | #content { 189 | margin-left: 14em; 190 | padding: 1em; 191 | width: 700px; 192 | border-left: 2px solid #cccccc; 193 | border-right: 2px solid #cccccc; 194 | background-color: #ffffff; 195 | } 196 | 197 | #about { 198 | clear: both; 199 | padding: 5px; 200 | border-top: 2px solid #cccccc; 201 | background-color: #ffffff; 202 | } 203 | 204 | @media print { 205 | body { 206 | font: 12pt "Times New Roman", "TimeNR", Times, serif; 207 | } 208 | a { font-weight: bold; color: #004080; text-decoration: underline; } 209 | 210 | #main { 211 | background-color: #ffffff; 212 | border-left: 0px; 213 | } 214 | 215 | #container { 216 | margin-left: 2%; 217 | margin-right: 2%; 218 | background-color: #ffffff; 219 | } 220 | 221 | #content { 222 | padding: 1em; 223 | background-color: #ffffff; 224 | } 225 | 226 | #navigation { 227 | display: none; 228 | } 229 | pre.example { 230 | font-family: "Andale Mono", monospace; 231 | font-size: 10pt; 232 | page-break-inside: avoid; 233 | } 234 | } 235 | 236 | table.module_list { 237 | border-width: 1px; 238 | border-style: solid; 239 | border-color: #cccccc; 240 | border-collapse: collapse; 241 | } 242 | table.module_list td { 243 | border-width: 1px; 244 | padding: 3px; 245 | border-style: solid; 246 | border-color: #cccccc; 247 | } 248 | table.module_list td.name { background-color: #f0f0f0; min-width: 200px; } 249 | table.module_list td.summary { width: 100%; } 250 | 251 | 252 | table.function_list { 253 | border-width: 1px; 254 | border-style: solid; 255 | border-color: #cccccc; 256 | border-collapse: collapse; 257 | } 258 | table.function_list td { 259 | border-width: 1px; 260 | padding: 3px; 261 | border-style: solid; 262 | border-color: #cccccc; 263 | } 264 | table.function_list td.name { background-color: #f0f0f0; min-width: 200px; } 265 | table.function_list td.summary { width: 100%; } 266 | 267 | ul.nowrap { 268 | overflow:auto; 269 | white-space:nowrap; 270 | } 271 | 272 | dl.table dt, dl.function dt {border-top: 1px solid #ccc; padding-top: 1em;} 273 | dl.table dd, dl.function dd {padding-bottom: 1em; margin: 10px 0 0 20px;} 274 | dl.table h3, dl.function h3 {font-size: .95em;} 275 | 276 | /* stop sublists from having initial vertical space */ 277 | ul ul { margin-top: 0px; } 278 | ol ul { margin-top: 0px; } 279 | ol ol { margin-top: 0px; } 280 | ul ol { margin-top: 0px; } 281 | 282 | /* make the target distinct; helps when we're navigating to a function */ 283 | a:target + * { 284 | background-color: #FF9; 285 | } 286 | 287 | 288 | /* styles for prettification of source */ 289 | pre .comment { color: #558817; } 290 | pre .constant { color: #a8660d; } 291 | pre .escape { color: #844631; } 292 | pre .keyword { color: #aa5050; font-weight: bold; } 293 | pre .library { color: #0e7c6b; } 294 | pre .marker { color: #512b1e; background: #fedc56; font-weight: bold; } 295 | pre .string { color: #8080ff; } 296 | pre .number { color: #f8660d; } 297 | pre .operator { color: #2239a8; font-weight: bold; } 298 | pre .preprocessor, pre .prepro { color: #a33243; } 299 | pre .global { color: #800080; } 300 | pre .user-keyword { color: #800080; } 301 | pre .prompt { color: #558817; } 302 | pre .url { color: #272fc2; text-decoration: underline; } 303 | 304 | -------------------------------------------------------------------------------- /nakama/engine/defold.lua: -------------------------------------------------------------------------------- 1 | --[[-- 2 | Nakama defold integration. 3 | 4 | @module nakama.engine.defold 5 | ]] 6 | 7 | local log = require "nakama.util.log" 8 | local b64 = require "nakama.util.b64" 9 | local uri = require "nakama.util.uri" 10 | local json = require "nakama.util.json" 11 | local uuid = require "nakama.util.uuid" 12 | 13 | b64.encode = _G.crypt and _G.crypt.encode_base64 or b64.encode 14 | b64.decode = _G.crypt and _G.crypt.decode_base64 or b64.decode 15 | 16 | local b64_encode = b64.encode 17 | local b64_decode = b64.decode 18 | local uri_encode_component = uri.encode_component 19 | local uri_decode_component = uri.decode_component 20 | local uri_encode = uri.encode 21 | local uri_decode = uri.decode 22 | 23 | uuid.seed() 24 | 25 | -- replace Lua based json.encode and decode with native Defold functions 26 | -- native json.encode function was added in Defold 1.3.7 27 | -- native json.decode function has been included in Defold "forever" 28 | json.encode = _G.json and _G.json.encode or json.encode 29 | json.decode = _G.json and _G.json.decode or json.decode 30 | 31 | local M = {} 32 | 33 | --- Get the device's mac address. 34 | -- @return The mac address string. 35 | local function get_mac_address() 36 | local ifaddrs = sys.get_ifaddrs() 37 | for _,interface in ipairs(ifaddrs) do 38 | if interface.mac then 39 | return interface.mac 40 | end 41 | end 42 | return nil 43 | end 44 | 45 | --- Returns a UUID from the device's mac address. 46 | -- @return The UUID string. 47 | function M.uuid() 48 | local mac = get_mac_address() 49 | if not mac then 50 | log("Unable to get hardware mac address for UUID") 51 | end 52 | return uuid(mac) 53 | end 54 | 55 | 56 | local make_http_request 57 | make_http_request = function(url, method, callback, headers, post_data, options, retry_intervals, retry_count, cancellation_token) 58 | if cancellation_token and cancellation_token.cancelled then 59 | callback(nil) 60 | return 61 | end 62 | http.request(url, method, function(self, id, result) 63 | if cancellation_token and cancellation_token.cancelled then 64 | callback(nil) 65 | return 66 | end 67 | log(result.response) 68 | local ok, decoded = pcall(json.decode, result.response) 69 | -- return result if everything is ok 70 | if ok and result.status >= 200 and result.status <= 299 then 71 | result.response = decoded 72 | callback(result.response) 73 | return 74 | end 75 | 76 | -- return the error if there are no more retries 77 | if retry_count > #retry_intervals then 78 | if not ok then 79 | result.response = { error = true, message = "Unable to decode response" } 80 | else 81 | result.response = { error = decoded.error or true, message = decoded.message, code = decoded.code } 82 | end 83 | callback(result.response) 84 | return 85 | end 86 | 87 | -- retry! 88 | local retry_interval = retry_intervals[retry_count] 89 | timer.delay(retry_interval, false, function() 90 | make_http_request(url, method, callback, headers, post_data, options, retry_intervals, retry_count + 1, cancellation_token) 91 | end) 92 | end, headers, post_data, options) 93 | 94 | end 95 | 96 | 97 | 98 | --- Make a HTTP request. 99 | -- @param config The http config table, see Defold docs. 100 | -- @param url_path The request URL. 101 | -- @param query_params Query params string. 102 | -- @param method The HTTP method string. 103 | -- @param post_data String of post data. 104 | -- @param callback The callback function. 105 | -- @return The mac address string. 106 | function M.http(config, url_path, query_params, method, post_data, retry_policy, cancellation_token, callback) 107 | local query_string = "" 108 | if next(query_params) then 109 | for query_key,query_value in pairs(query_params) do 110 | if type(query_value) == "table" then 111 | for _,v in ipairs(query_value) do 112 | query_string = ("%s%s%s=%s"):format(query_string, (#query_string == 0 and "?" or "&"), query_key, uri_encode_component(tostring(v))) 113 | end 114 | else 115 | query_string = ("%s%s%s=%s"):format(query_string, (#query_string == 0 and "?" or "&"), query_key, uri_encode_component(tostring(query_value))) 116 | end 117 | end 118 | end 119 | local url = ("%s%s%s"):format(config.http_uri, url_path, query_string) 120 | 121 | local headers = {} 122 | headers["Accept"] = "application/json" 123 | headers["Content-Type"] = "application/json" 124 | if config.bearer_token then 125 | headers["Authorization"] = ("Bearer %s"):format(config.bearer_token) 126 | elseif config.username then 127 | local credentials = b64_encode(config.username .. ":" .. config.password) 128 | headers["Authorization"] = ("Basic %s"):format(credentials) 129 | end 130 | 131 | local options = { 132 | timeout = config.timeout 133 | } 134 | 135 | log("HTTP", method, url) 136 | log("DATA", post_data) 137 | make_http_request(url, method, callback, headers, post_data, options, retry_policy or config.retry_policy, 1, cancellation_token) 138 | end 139 | 140 | --- Create a new socket with message handler. 141 | -- @param config The socket config table, see Defold docs. 142 | -- @param on_message Your function to process socket messages. 143 | -- @return A socket table. 144 | function M.socket_create(config, on_message) 145 | assert(config, "You must provide a config") 146 | assert(on_message, "You must provide a message handler") 147 | 148 | local socket = {} 149 | socket.config = config 150 | socket.scheme = config.use_ssl and "wss" or "ws" 151 | socket.on_message = on_message 152 | 153 | return socket 154 | end 155 | 156 | -- internal on_message, calls user defined socket.on_message function 157 | local function on_message(socket, message) 158 | message = json.decode(message) 159 | socket.on_message(socket, message) 160 | end 161 | 162 | --- Connect a created socket using web sockets. 163 | -- @param socket The socket table, see socket_create. 164 | -- @param callback The callback function. 165 | function M.socket_connect(socket, callback) 166 | assert(socket) 167 | assert(callback) 168 | 169 | local url = ("%s://%s:%d/ws?token=%s"):format(socket.scheme, socket.config.host, socket.config.port, uri.encode_component(socket.config.bearer_token)) 170 | --const url = `${scheme}${this.host}:${this.port}/ws?lang=en&status=${encodeURIComponent(createStatus.toString())}&token=${encodeURIComponent(session.token)}`; 171 | 172 | log(url) 173 | 174 | local params = { 175 | protocol = nil, 176 | headers = nil, 177 | timeout = (socket.config.timeout or 0) * 1000, 178 | } 179 | socket.connection = websocket.connect(url, params, function(self, conn, data) 180 | if data.event == websocket.EVENT_CONNECTED then 181 | log("EVENT_CONNECTED") 182 | callback(true) 183 | elseif data.event == websocket.EVENT_DISCONNECTED then 184 | log("EVENT_DISCONNECTED: ", data.message) 185 | if socket.on_disconnect then socket.on_disconnect() end 186 | elseif data.event == websocket.EVENT_ERROR then 187 | log("EVENT_ERROR: ", data.message or data.error) 188 | callback(false, data.message or data.error) 189 | elseif data.event == websocket.EVENT_MESSAGE then 190 | log("EVENT_MESSAGE: ", data.message) 191 | on_message(socket, data.message) 192 | end 193 | end) 194 | end 195 | 196 | 197 | --- Disconnect a created socket 198 | -- @param socket The socket table, see socket_create. 199 | function M.socket_disconnect(socket) 200 | assert(socket) 201 | if socket.connection then 202 | websocket.disconnect(socket.connection) 203 | socket.connection = nil 204 | end 205 | end 206 | 207 | --- Send a socket message. 208 | -- @param socket The socket table, see socket_create. 209 | -- @param message The message string to send. 210 | function M.socket_send(socket, message) 211 | assert(socket and socket.connection, "You must provide a socket") 212 | assert(message, "You must provide a message to send") 213 | 214 | local data = json.encode(message) 215 | -- Fix encoding of match_create and status_update messages to send {} instead of [] 216 | if message.match_create ~= nil or message.status_update ~= nil then 217 | data = string.gsub(data, "%[%]", "{}") 218 | end 219 | 220 | local options = { 221 | type = websocket.DATA_TYPE_TEXT 222 | } 223 | websocket.send(socket.connection, data, options) 224 | end 225 | 226 | return M 227 | -------------------------------------------------------------------------------- /docs/modules/nakama.util.uuid.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | SDK API documentation 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 52 | 53 |
54 | 55 |

Module nakama.util.uuid

56 |

Work with universally unique identifiers (UUIDs).

57 |

58 |

Copyright 2012 Rackspace (original), 2013 Thijs Schreijer (modifications), 59 | 2020 60 |

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 |

84 | 85 | 86 |

Functions

87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 |
new (hwaddr)Creates a new uuid.
randomseed (seed)Improved randomseed function.
seed ()Seeds the random generator.
101 | 102 |
103 |
104 | 105 | 106 |

Functions

107 | 108 |
109 |
110 | 111 | new (hwaddr) 112 |
113 |
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 |
  • 132 |
133 | 134 |

Returns:

135 |
    136 | 137 | a properly formatted uuid string 138 |
139 | 140 | 141 | 142 |

Usage:

143 |
    144 |
    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 |
  • 167 |
168 | 169 |

Returns:

170 |
    171 | 172 | the (potentially modified) seed used 173 |
174 | 175 | 176 | 177 |

Usage:

178 |
    179 |
    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 | ![](https://img.shields.io/badge/Nakama%20gRPC%20-3.19.0-green) 2 | ![](https://img.shields.io/badge/Nakama%20RT%20-1.30.0-green) 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 | --------------------------------------------------------------------------------