{"action":"created","comment":{"url":"https://api.github.com/repos/heroiclabs/nakama/pulls/comments/166411603","pull_request_review_id":94469557,"id":166411603,"diff_hunk":"@@ -0,0 +1,1108 @@\n+// Copyright 2018 The Nakama Authors\n+//\n+// Licensed under the Apache License, Version 2.0 (the \"License\");\n+// you may not use this file except in compliance with the License.\n+// You may obtain a copy of the License at\n+//\n+// http://www.apache.org/licenses/LICENSE-2.0\n+//\n+// Unless required by applicable law or agreed to in writing, software\n+// distributed under the License is distributed on an \"AS IS\" BASIS,\n+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n+// See the License for the specific language governing permissions and\n+// limitations under the License.\n+\n+package server\n+\n+import (\n+\t\"context\"\n+\t\"io\"\n+\t\"net/http\"\n+\t\"time\"\n+\n+\t\"strings\"\n+\n+\t\"database/sql\"\n+\n+\t\"encoding/json\"\n+\n+\t\"encoding/base64\"\n+\n+\t\"encoding/hex\"\n+\t\"io/ioutil\"\n+\n+\t\"github.com/gorhill/cronexpr\"\n+\t\"github.com/satori/go.uuid\"\n+\t\"github.com/yuin/gopher-lua\"\n+\t\"go.uber.org/zap\"\n+\t\"github.com/heroiclabs/nakama/rtapi\"\n+\t\"golang.org/x/crypto/bcrypt\"\n+\t\"sync\"\n+\t\"crypto/aes\"\n+\t\"crypto/rand\"\n+\t\"crypto/cipher\"\n+)\n+\n+const CALLBACKS = \"runtime_callbacks\"\n+\n+type Callbacks struct {\n+\tRPC    map[string]*lua.LFunction\n+}\n+\n+type NakamaModule struct {\n+\tlogger              *zap.Logger\n+\tdb                  *sql.DB\n+\tconfig              Config\n+\tregistry            *SessionRegistry\n+\ttracker             Tracker\n+\trouter              MessageRouter\n+\tonce                *sync.Once\n+\tannounceRPC         func(string)\n+\tclient              *http.Client\n+}\n+\n+func NewNakamaModule(logger *zap.Logger, db *sql.DB, config Config, l *lua.LState, registry *SessionRegistry, tracker Tracker, router MessageRouter, once *sync.Once, announceRPC func(string)) *NakamaModule {\n+\tl.SetContext(context.WithValue(context.Background(), CALLBACKS, &Callbacks{\n+\t\tRPC:    make(map[string]*lua.LFunction),\n+\t}))\n+\treturn &NakamaModule{\n+\t\tlogger:      logger,\n+\t\tdb:          db,\n+\t\tconfig:      config,\n+\t\tregistry:    registry,\n+\t\ttracker:     tracker,\n+\t\trouter:      router,\n+\t\tonce:        once,\n+\t\tannounceRPC: announceRPC,\n+\t\tclient: &http.Client{\n+\t\t\tTimeout: 5 * time.Second,\n+\t\t},\n+\t}\n+}\n+\n+func (n *NakamaModule) Loader(l *lua.LState) int {\n+\tmod := l.SetFuncs(l.NewTable(), map[string]lua.LGFunction{\n+\t\t\"sql_exec\":             n.sqlExec,\n+\t\t\"sql_query\":            n.sqlQuery,\n+\t\t\"uuid_v4\":              n.uuidV4,\n+\t\t\"uuid_bytes_to_string\": n.uuidBytesToString,\n+\t\t\"uuid_string_to_bytes\": n.uuidStringToBytes,\n+\t\t\"http_request\":         n.httpRequest,\n+\t\t\"json_encode\":          n.jsonEncode,\n+\t\t\"json_decode\":          n.jsonDecode,\n+\t\t\"base64_encode\":        n.base64Encode,\n+\t\t\"base64_decode\":        n.base64Decode,\n+\t\t\"base16_encode\":        n.base16Encode,\n+\t\t\"base16_decode\":        n.base16decode,\n+\t\t\"aes128_encrypt\":       n.aes128encrypt,\n+\t\t\"aes128_decrypt\":       n.aes128decrypt,\n+\t\t\"bcrypt_hash\":          n.bcryptHash,\n+\t\t\"bcrypt_compare\":       n.bcryptCompare,\n+\t\t\"generate_auth_token\":  n.generateAuthToken,\n+\t\t\"cron_next\":            n.cronNext,\n+\t\t\"logger_info\":          n.loggerInfo,\n+\t\t\"logger_warn\":          n.loggerWarn,\n+\t\t\"logger_error\":         n.loggerError,\n+\t\t\"stream_user_get\":      n.streamUserGet,\n+\t\t\"stream_user_join\":     n.streamUserJoin,\n+\t\t\"stream_user_leave\":    n.streamUserLeave,\n+\t\t\"stream_count\":         n.streamCount,\n+\t\t\"stream_close\":         n.streamClose,\n+\t\t\"stream_send\":          n.streamSend,\n+\t\t\"register_rpc\":         n.registerRPC,\n+\t\t\"run_once\":             n.runOnce,\n+\t})\n+\n+\tl.Push(mod)\n+\treturn 1\n+}\n+\n+func (n *NakamaModule) sqlExec(l *lua.LState) int {\n+\tquery := l.CheckString(1)\n+\tif query == \"\" {\n+\t\tl.ArgError(1, \"expects query string\")\n+\t\treturn 0\n+\t}\n+\tparamsTable := l.OptTable(2, l.NewTable())\n+\tif paramsTable == nil {\n+\t\tl.ArgError(2, \"expects params table\")\n+\t\treturn 0\n+\t}\n+\tvar params []interface{}\n+\tif paramsTable.Len() != 0 {\n+\t\tvar ok bool\n+\t\tparams, ok = convertLuaValue(paramsTable).([]interface{})\n+\t\tif !ok {\n+\t\t\tl.ArgError(2, \"expects a list of params as a table\")\n+\t\t\treturn 0\n+\t\t}\n+\t}\n+\n+\tresult, err := n.db.Exec(query, params...)\n+\tif err != nil {\n+\t\tl.RaiseError(\"sql exec error: %v\", err.Error())\n+\t\treturn 0\n+\t}\n+\tcount, err := result.RowsAffected()\n+\tif err != nil {\n+\t\tl.RaiseError(\"sql exec rows affected error: %v\", err.Error())\n+\t\treturn 0\n+\t}\n+\n+\tl.Push(lua.LNumber(count))\n+\treturn 1\n+}\n+\n+func (n *NakamaModule) sqlQuery(l *lua.LState) int {\n+\tquery := l.CheckString(1)\n+\tif query == \"\" {\n+\t\tl.ArgError(1, \"expects query string\")\n+\t\treturn 0\n+\t}\n+\tparamsTable := l.OptTable(2, l.NewTable())\n+\tif paramsTable == nil {\n+\t\tl.ArgError(2, \"expects params table\")\n+\t\treturn 0\n+\t}\n+\tvar params []interface{}\n+\tif paramsTable.Len() != 0 {\n+\t\tvar ok bool\n+\t\tparams, ok = convertLuaValue(paramsTable).([]interface{})\n+\t\tif !ok {\n+\t\t\tl.ArgError(2, \"expects a list of params as a table\")\n+\t\t\treturn 0\n+\t\t}\n+\t}\n+\n+\trows, err := n.db.Query(query, params...)\n+\tif err != nil {\n+\t\tl.RaiseError(\"sql query error: %v\", err.Error())\n+\t\treturn 0\n+\t}\n+\tdefer rows.Close()\n+\n+\tresultColumns, err := rows.Columns()\n+\tif err != nil {\n+\t\tl.RaiseError(\"sql query column lookup error: %v\", err.Error())\n+\t\treturn 0\n+\t}\n+\tresultColumnCount := len(resultColumns)\n+\tresultRows := make([][]interface{}, 0)\n+\tfor rows.Next() {\n+\t\tresultRowValues := make([]interface{}, resultColumnCount)\n+\t\tresultRowPointers := make([]interface{}, resultColumnCount)\n+\t\tfor i, _ := range resultRowValues {\n+\t\t\tresultRowPointers[i] = &resultRowValues[i]\n+\t\t}\n+\t\tif err = rows.Scan(resultRowPointers...); err != nil {\n+\t\t\tl.RaiseError(\"sql query scan error: %v\", err.Error())\n+\t\t\treturn 0\n+\t\t}\n+\t\tresultRows = append(resultRows, resultRowValues)\n+\t}\n+\tif err = rows.Err(); err != nil {\n+\t\tl.RaiseError(\"sql query row scan error: %v\", err.Error())\n+\t\treturn 0\n+\t}\n+\n+\trt := l.NewTable()\n+\tfor i, r := range resultRows {\n+\t\trowTable := l.NewTable()\n+\t\tfor j, col := range resultColumns {\n+\t\t\trowTable.RawSetString(col, convertValue(l, r[j]))\n+\t\t}\n+\t\trt.RawSetInt(i+1, rowTable)\n+\t}\n+\tl.Push(rt)\n+\treturn 1\n+}\n+\n+func (n *NakamaModule) uuidV4(l *lua.LState) int {\n+\tl.Push(lua.LString(uuid.NewV4().String()))\n+\treturn 1\n+}\n+\n+func (n *NakamaModule) uuidBytesToString(l *lua.LState) int {\n+\tuuidBytes := l.CheckString(1)\n+\tif uuidBytes == \"\" {\n+\t\tl.ArgError(1, \"expects a UUID byte string\")\n+\t\treturn 0\n+\t}\n+\tu, err := uuid.FromBytes([]byte(uuidBytes))\n+\tif err != nil {\n+\t\tl.ArgError(1, \"not a valid UUID byte string\")\n+\t\treturn 0\n+\t}\n+\tl.Push(lua.LString(u.String()))\n+\treturn 1\n+}\n+\n+func (n *NakamaModule) uuidStringToBytes(l *lua.LState) int {\n+\tuuidString := l.CheckString(1)\n+\tif uuidString == \"\" {\n+\t\tl.ArgError(1, \"expects a UUID string\")\n+\t\treturn 0\n+\t}\n+\tu, err := uuid.FromString(uuidString)\n+\tif err != nil {\n+\t\tl.ArgError(1, \"not a valid UUID string\")\n+\t\treturn 0\n+\t}\n+\tl.Push(lua.LString(u.Bytes()))\n+\treturn 1\n+}\n+\n+func (n *NakamaModule) httpRequest(l *lua.LState) int {\n+\turl := l.CheckString(1)\n+\tmethod := l.CheckString(2)\n+\theaders := l.CheckTable(3)\n+\tbody := l.OptString(4, \"\")\n+\tif url == \"\" {\n+\t\tl.ArgError(1, \"expects URL string\")\n+\t\treturn 0\n+\t}\n+\tif method == \"\" {\n+\t\tl.ArgError(2, \"expects method string\")\n+\t\treturn 0\n+\t}\n+\n+\t// Prepare request body, if any.\n+\tvar requestBody io.Reader\n+\tif body != \"\" {\n+\t\trequestBody = strings.NewReader(body)\n+\t}\n+\t// Prepare the request.\n+\treq, err := http.NewRequest(method, url, requestBody)\n+\tif err != nil {\n+\t\tl.RaiseError(\"HTTP request error: %v\", err.Error())\n+\t\treturn 0\n+\t}\n+\t// Apply any request headers.\n+\thttpHeaders := ConvertLuaTable(headers)\n+\tfor k, v := range httpHeaders {\n+\t\tif vs, ok := v.(string); !ok {\n+\t\t\tl.RaiseError(\"HTTP header values must be strings\")\n+\t\t\treturn 0\n+\t\t} else {\n+\t\t\treq.Header.Add(k, vs)\n+\t\t}\n+\t}\n+\t// Execute the request.\n+\tresp, err := n.client.Do(req)\n+\tif err != nil {\n+\t\tl.RaiseError(\"HTTP request error: %v\", err.Error())\n+\t\treturn 0\n+\t}\n+\t// Read the response body.\n+\tresponseBody, err := ioutil.ReadAll(resp.Body)\n+\tresp.Body.Close()\n+\tif err != nil {\n+\t\tl.RaiseError(\"HTTP response body error: %v\", err.Error())\n+\t\treturn 0\n+\t}\n+\t// Read the response headers.\n+\tresponseHeaders := make(map[string]interface{}, len(resp.Header))\n+\tfor k, vs := range resp.Header {\n+\t\t// TODO accept multiple values per header\n+\t\tfor _, v := range vs {\n+\t\t\tresponseHeaders[k] = v\n+\t\t\tbreak\n+\t\t}\n+\t}\n+\n+\tl.Push(lua.LNumber(resp.StatusCode))\n+\tl.Push(ConvertMap(l, responseHeaders))\n+\tl.Push(lua.LString(string(responseBody)))\n+\treturn 3\n+}\n+\n+func (n *NakamaModule) jsonEncode(l *lua.LState) int {\n+\tjsonTable := l.Get(1)\n+\tif jsonTable == nil {\n+\t\tl.ArgError(1, \"expects a non-nil value to encode\")\n+\t\treturn 0\n+\t}\n+\n+\tjsonData := convertLuaValue(jsonTable)\n+\tjsonBytes, err := json.Marshal(jsonData)\n+\tif err != nil {\n+\t\tl.RaiseError(\"error encoding to JSON: %v\", err.Error())\n+\t\treturn 0\n+\t}\n+\n+\tl.Push(lua.LString(string(jsonBytes)))\n+\treturn 1\n+}\n+\n+func (n *NakamaModule) jsonDecode(l *lua.LState) int {\n+\tjsonString := l.CheckString(1)\n+\tif jsonString == \"\" {\n+\t\tl.ArgError(1, \"expects JSON string\")\n+\t\treturn 0\n+\t}\n+\n+\tvar jsonData interface{}\n+\tif err := json.Unmarshal([]byte(jsonString), &jsonData); err != nil {\n+\t\tl.RaiseError(\"not a valid JSON string: %v\", err.Error())\n+\t\treturn 0\n+\t}\n+\n+\tl.Push(convertValue(l, jsonData))\n+\treturn 1\n+}\n+\n+func (n *NakamaModule) base64Encode(l *lua.LState) int {\n+\tinput := l.CheckString(1)\n+\tif input == \"\" {\n+\t\tl.ArgError(1, \"expects string\")\n+\t\treturn 0\n+\t}\n+\n+\toutput := base64.StdEncoding.EncodeToString([]byte(input))\n+\tl.Push(lua.LString(output))\n+\treturn 1\n+}\n+\n+func (n *NakamaModule) base64Decode(l *lua.LState) int {\n+\tinput := l.CheckString(1)\n+\tif input == \"\" {\n+\t\tl.ArgError(1, \"expects string\")\n+\t\treturn 0\n+\t}\n+\n+\toutput, err := base64.RawStdEncoding.DecodeString(input)\n+\tif err != nil {\n+\t\tl.RaiseError(\"not a valid base64 string: %v\", err.Error())\n+\t\treturn 0\n+\t}\n+\n+\tl.Push(lua.LString(output))\n+\treturn 1\n+}\n+\n+func (n *NakamaModule) base16Encode(l *lua.LState) int {\n+\tinput := l.CheckString(1)\n+\tif input == \"\" {\n+\t\tl.ArgError(1, \"expects string\")\n+\t\treturn 0\n+\t}\n+\n+\toutput := hex.EncodeToString([]byte(input))\n+\tl.Push(lua.LString(output))\n+\treturn 1\n+}\n+\n+func (n *NakamaModule) base16decode(l *lua.LState) int {\n+\tinput := l.CheckString(1)\n+\tif input == \"\" {\n+\t\tl.ArgError(1, \"expects string\")\n+\t\treturn 0\n+\t}\n+\n+\toutput, err := hex.DecodeString(input)\n+\tif err != nil {\n+\t\tl.RaiseError(\"not a valid base16 string: %v\", err.Error())\n+\t\treturn 0\n+\t}\n+\n+\tl.Push(lua.LString(output))\n+\treturn 1\n+}\n+\n+func (n *NakamaModule) aes128encrypt(l *lua.LState) int {\n+\tinput := l.CheckString(1)\n+\tif input == \"\" {\n+\t\tl.ArgError(1, \"expects string\")\n+\t\treturn 0\n+\t}\n+\tkey := l.CheckString(2)\n+\tif len(key) != 16 {\n+\t\tl.ArgError(2, \"expects key 16 bytes long\")\n+\t\treturn 0\n+\t}\n+\n+\t// Pad string up to length multiple of 4 if needed.\n+\tif maybePad := len(input) % 4; maybePad != 0 {\n+\t\tinput += strings.Repeat(\" \", 4-maybePad)\n+\t}\n+\n+\tblock, err := aes.NewCipher([]byte(key))\n+\tif err != nil {\n+\t\tl.RaiseError(\"error creating cipher block: %v\", err.Error())\n+\t\treturn 0\n+\t}\n+\n+\tcipherText := make([]byte, aes.BlockSize+len(input))\n+\tiv := cipherText[:aes.BlockSize]\n+\tif _, err = io.ReadFull(rand.Reader, iv); err != nil {\n+\t\tl.RaiseError(\"error getting iv: %v\", err.Error())\n+\t\treturn 0\n+\t}\n+\n+\tstream := cipher.NewCFBEncrypter(block, iv)\n+\tstream.XORKeyStream(cipherText[aes.BlockSize:], []byte(input))\n+\n+\tl.Push(lua.LString(cipherText))\n+\treturn 1\n+}\n+\n+func (n *NakamaModule) aes128decrypt(l *lua.LState) int {\n+\tinput := l.CheckString(1)\n+\tif input == \"\" {\n+\t\tl.ArgError(1, \"expects string\")\n+\t\treturn 0\n+\t}\n+\tkey := l.CheckString(2)\n+\tif len(key) != 16 {\n+\t\tl.ArgError(2, \"expects key 16 bytes long\")\n+\t\treturn 0\n+\t}\n+\n+\tif len(input) < aes.BlockSize {\n+\t\tl.RaiseError(\"input too short\")\n+\t\treturn 0\n+\t}\n+\n+\tblock, err := aes.NewCipher([]byte(key))\n+\tif err != nil {\n+\t\tl.RaiseError(\"error creating cipher block: %v\", err.Error())\n+\t\treturn 0\n+\t}\n+\n+\tcipherText := []byte(input)\n+\tiv := cipherText[:aes.BlockSize]\n+\tcipherText = cipherText[aes.BlockSize:]\n+\n+\tstream := cipher.NewCFBDecrypter(block, iv)\n+\tstream.XORKeyStream(cipherText, cipherText)\n+\n+\tl.Push(lua.LString(cipherText))\n+\treturn 1\n+}\n+\n+func (n *NakamaModule) bcryptHash(l *lua.LState) int {\n+\tinput := l.CheckString(1)\n+\tif input == \"\" {\n+\t\tl.ArgError(1, \"expects string\")\n+\t\treturn 0\n+\t}\n+\n+\thash, err := bcrypt.GenerateFromPassword([]byte(input), bcrypt.DefaultCost)\n+\tif err != nil {\n+\t\tl.RaiseError(\"error hashing input: %v\", err.Error())\n+\t\treturn 0\n+\t}\n+\n+\tl.Push(lua.LString(hash))\n+\treturn 1\n+}\n+\n+func (n *NakamaModule) bcryptCompare(l *lua.LState) int {\n+\thash := l.CheckString(1)\n+\tif hash == \"\" {\n+\t\tl.ArgError(1, \"expects string\")\n+\t\treturn 0\n+\t}\n+\tplaintext := l.CheckString(2)\n+\tif plaintext == \"\" {\n+\t\tl.ArgError(2, \"expects string\")\n+\t\treturn 0\n+\t}\n+\n+\terr := bcrypt.CompareHashAndPassword([]byte(hash), []byte(plaintext))\n+\tif err == nil {\n+\t\tl.Push(lua.LBool(true))\n+\t\treturn 1\n+\t} else if err == bcrypt.ErrHashTooShort || err == bcrypt.ErrMismatchedHashAndPassword {\n+\t\tl.Push(lua.LBool(false))\n+\t\treturn 1\n+\t}\n+\n+\tl.RaiseError(\"error comparing hash and plaintext: %v\", err.Error())\n+\treturn 0\n+}\n+\n+func (n *NakamaModule) generateAuthToken(l *lua.LState) int {\n+\t// Parse input User ID.\n+\tuserIDString := l.CheckString(1)\n+\tif userIDString == \"\" {\n+\t\tl.ArgError(1, \"expects user id\")\n+\t\treturn 0\n+\t}\n+\t_, err := uuid.FromString(userIDString)\n+\tif err != nil {\n+\t\tl.ArgError(1, \"expects valid user id\")\n+\t\treturn 0\n+\t}\n+\n+\t// Input username.\n+\tusername := l.CheckString(2)\n+\tif username == \"\" {\n+\t\tl.ArgError(2, \"expects username\")\n+\t\treturn 0\n+\t}\n+\n+\texp := l.CheckInt64(3)\n+\tif exp == 0 {\n+\t\t// If expiry is 0 or not set, use standard configured expiry.\n+\t\texp = time.Now().UTC().Add(time.Duration(n.config.GetSession().TokenExpiryMs) * time.Millisecond).Unix()\n+\t}\n+\n+\ttoken := generateTokenWithExpiry(n.config, userIDString, username, exp)\n+\n+\tl.Push(lua.LString(token))\n+\treturn 1\n+}\n+\n+func (n *NakamaModule) cronNext(l *lua.LState) int {\n+\tcron := l.CheckString(1)\n+\tif cron == \"\" {\n+\t\tl.ArgError(1, \"expects cron string\")\n+\t\treturn 0\n+\t}\n+\tts := l.CheckInt64(2)\n+\tif ts == 0 {\n+\t\tl.ArgError(1, \"expects timestamp in seconds\")\n+\t\treturn 0\n+\t}\n+\n+\texpr, err := cronexpr.Parse(cron)\n+\tif err != nil {\n+\t\tl.ArgError(1, \"expects a valid cron string\")\n+\t\treturn 0\n+\t}\n+\tt := time.Unix(ts, 0).UTC()\n+\tnext := expr.Next(t)\n+\tnextTs := next.UTC().Unix()\n+\tl.Push(lua.LNumber(nextTs))\n+\treturn 1\n+}\n+\n+func (n *NakamaModule) loggerInfo(l *lua.LState) int {\n+\tmessage := l.CheckString(1)\n+\tif message == \"\" {\n+\t\tl.ArgError(1, \"expects message string\")\n+\t\treturn 0\n+\t}\n+\tn.logger.Info(message)\n+\tl.Push(lua.LString(message))\n+\treturn 1\n+}\n+\n+func (n *NakamaModule) loggerWarn(l *lua.LState) int {\n+\tmessage := l.CheckString(1)\n+\tif message == \"\" {\n+\t\tl.ArgError(1, \"expects message string\")\n+\t\treturn 0\n+\t}\n+\tn.logger.Warn(message)\n+\tl.Push(lua.LString(message))\n+\treturn 1\n+}\n+\n+func (n *NakamaModule) loggerError(l *lua.LState) int {\n+\tmessage := l.CheckString(1)\n+\tif message == \"\" {\n+\t\tl.ArgError(1, \"expects message string\")\n+\t\treturn 0\n+\t}\n+\tn.logger.Error(message)\n+\tl.Push(lua.LString(message))\n+\treturn 1\n+}\n+\n+func (n *NakamaModule) streamUserGet(l *lua.LState) int {\n+\t// Parse input User ID.\n+\tuserIDString := l.CheckString(1)\n+\tif userIDString == \"\" {\n+\t\tl.ArgError(1, \"expects user id\")\n+\t\treturn 0\n+\t}\n+\tuserID, err := uuid.FromString(userIDString)\n+\tif err != nil {\n+\t\tl.ArgError(1, \"expects valid user id\")\n+\t\treturn 0\n+\t}\n+\n+\t// Parse input Session ID.\n+\tsessionIDString := l.CheckString(2)\n+\tif sessionIDString == \"\" {\n+\t\tl.ArgError(2, \"expects session id\")\n+\t\treturn 0\n+\t}\n+\tsessionID, err := uuid.FromString(sessionIDString)\n+\tif err != nil {\n+\t\tl.ArgError(2, \"expects valid session id\")\n+\t\treturn 0\n+\t}\n+\n+\t// Parse input stream identifier.\n+\tstreamTable := l.CheckTable(3)\n+\tif streamTable == nil || streamTable.Len() == 0 {\n+\t\tl.ArgError(3, \"expects a valid stream\")\n+\t\treturn 0\n+\t}\n+\tstream := PresenceStream{}\n+\tconversionError := \"\"\n+\tstreamTable.ForEach(func(k lua.LValue, v lua.LValue) {\n+\t\tswitch k.String() {\n+\t\tcase \"Mode\":\n+\t\t\tif v.Type() != lua.LTNumber {\n+\t\t\t\tconversionError = \"stream Mode must be a number\"\n+\t\t\t\treturn\n+\t\t\t}\n+\t\t\tstream.Mode = uint8(lua.LVAsNumber(v))\n+\t\tcase \"Subject\":\n+\t\t\tif v.Type() != lua.LTString {\n+\t\t\t\tconversionError = \"stream Subject must be a string\"\n+\t\t\t\treturn\n+\t\t\t}\n+\t\t\tsid, err := uuid.FromString(v.String())\n+\t\t\tif err != nil {\n+\t\t\t\tconversionError = \"stream Subject must be a valid identifier\"\n+\t\t\t\treturn\n+\t\t\t}\n+\t\t\tstream.Subject = sid\n+\t\tcase \"Descriptor\":\n+\t\t\tif v.Type() != lua.LTString {\n+\t\t\t\tconversionError = \"stream Descriptor must be a string\"\n+\t\t\t\treturn\n+\t\t\t}\n+\t\t\tdid, err := uuid.FromString(v.String())\n+\t\t\tif err != nil {\n+\t\t\t\tconversionError = \"stream Descriptor must be a valid identifier\"\n+\t\t\t\treturn\n+\t\t\t}\n+\t\t\tstream.Subject = did\n+\t\tcase \"Label\":\n+\t\t\tif v.Type() != lua.LTString {\n+\t\t\t\tconversionError = \"stream Label must be a string\"\n+\t\t\t\treturn\n+\t\t\t}\n+\t\t\tstream.Label = v.String()\n+\t\t}\n+\t})\n+\tif conversionError != \"\" {\n+\t\tl.ArgError(3, conversionError)\n+\t\treturn 0\n+\t}\n+\n+\tmeta := n.tracker.GetLocalBySessionIDStreamUserID(sessionID, stream, userID)\n+\tif meta == nil {\n+\t\tl.Push(lua.LNil)\n+\t} else {\n+\t\tmetaTable := l.NewTable()\n+\t\tmetaTable.RawSetString(\"Hidden\", lua.LBool(meta.Hidden))","path":"server/runtime_nakama_module.go","position":695,"original_position":695,"commit_id":"d41b9f67e0ac8aaca9924f1a87aef34d346f0916","original_commit_id":"d41b9f67e0ac8aaca9924f1a87aef34d346f0916","user":{"login":"zyro","id":308795,"avatar_url":"https://avatars3.githubusercontent.com/u/308795?v=4","gravatar_id":"","url":"https://api.github.com/users/zyro","html_url":"https://github.com/zyro","followers_url":"https://api.github.com/users/zyro/followers","following_url":"https://api.github.com/users/zyro/following{/other_user}","gists_url":"https://api.github.com/users/zyro/gists{/gist_id}","starred_url":"https://api.github.com/users/zyro/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/zyro/subscriptions","organizations_url":"https://api.github.com/users/zyro/orgs","repos_url":"https://api.github.com/users/zyro/repos","events_url":"https://api.github.com/users/zyro/events{/privacy}","received_events_url":"https://api.github.com/users/zyro/received_events","type":"User","site_admin":false},"body":"To be determined when we wire up conversion of data, especially for before/after hooks.","created_at":"2018-02-06T19:14:40Z","updated_at":"2018-02-06T19:14:40Z","html_url":"https://github.com/heroiclabs/nakama/pull/145#discussion_r166411603","pull_request_url":"https://api.github.com/repos/heroiclabs/nakama/pulls/145","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/heroiclabs/nakama/pulls/comments/166411603"},"html":{"href":"https://github.com/heroiclabs/nakama/pull/145#discussion_r166411603"},"pull_request":{"href":"https://api.github.com/repos/heroiclabs/nakama/pulls/145"}},"in_reply_to_id":166334101},"pull_request":{"url":"https://api.github.com/repos/heroiclabs/nakama/pulls/145","id":166946610,"html_url":"https://github.com/heroiclabs/nakama/pull/145","diff_url":"https://github.com/heroiclabs/nakama/pull/145.diff","patch_url":"https://github.com/heroiclabs/nakama/pull/145.patch","issue_url":"https://api.github.com/repos/heroiclabs/nakama/issues/145","number":145,"state":"open","locked":false,"title":"Wire up runtime scripts","user":{"login":"zyro","id":308795,"avatar_url":"https://avatars3.githubusercontent.com/u/308795?v=4","gravatar_id":"","url":"https://api.github.com/users/zyro","html_url":"https://github.com/zyro","followers_url":"https://api.github.com/users/zyro/followers","following_url":"https://api.github.com/users/zyro/following{/other_user}","gists_url":"https://api.github.com/users/zyro/gists{/gist_id}","starred_url":"https://api.github.com/users/zyro/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/zyro/subscriptions","organizations_url":"https://api.github.com/users/zyro/orgs","repos_url":"https://api.github.com/users/zyro/repos","events_url":"https://api.github.com/users/zyro/events{/privacy}","received_events_url":"https://api.github.com/users/zyro/received_events","type":"User","site_admin":false},"body":"","created_at":"2018-02-03T04:50:36Z","updated_at":"2018-02-06T19:14:40Z","closed_at":null,"merged_at":null,"merge_commit_sha":"7c0df569ad70b24a613baa2a57e92ae16e85688c","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"milestone":null,"commits_url":"https://api.github.com/repos/heroiclabs/nakama/pulls/145/commits","review_comments_url":"https://api.github.com/repos/heroiclabs/nakama/pulls/145/comments","review_comment_url":"https://api.github.com/repos/heroiclabs/nakama/pulls/comments{/number}","comments_url":"https://api.github.com/repos/heroiclabs/nakama/issues/145/comments","statuses_url":"https://api.github.com/repos/heroiclabs/nakama/statuses/d41b9f67e0ac8aaca9924f1a87aef34d346f0916","head":{"label":"heroiclabs:aim-runtime-scripts","ref":"aim-runtime-scripts","sha":"d41b9f67e0ac8aaca9924f1a87aef34d346f0916","user":{"login":"heroiclabs","id":12753245,"avatar_url":"https://avatars0.githubusercontent.com/u/12753245?v=4","gravatar_id":"","url":"https://api.github.com/users/heroiclabs","html_url":"https://github.com/heroiclabs","followers_url":"https://api.github.com/users/heroiclabs/followers","following_url":"https://api.github.com/users/heroiclabs/following{/other_user}","gists_url":"https://api.github.com/users/heroiclabs/gists{/gist_id}","starred_url":"https://api.github.com/users/heroiclabs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/heroiclabs/subscriptions","organizations_url":"https://api.github.com/users/heroiclabs/orgs","repos_url":"https://api.github.com/users/heroiclabs/repos","events_url":"https://api.github.com/users/heroiclabs/events{/privacy}","received_events_url":"https://api.github.com/users/heroiclabs/received_events","type":"Organization","site_admin":false},"repo":{"id":78869430,"name":"nakama","full_name":"heroiclabs/nakama","owner":{"login":"heroiclabs","id":12753245,"avatar_url":"https://avatars0.githubusercontent.com/u/12753245?v=4","gravatar_id":"","url":"https://api.github.com/users/heroiclabs","html_url":"https://github.com/heroiclabs","followers_url":"https://api.github.com/users/heroiclabs/followers","following_url":"https://api.github.com/users/heroiclabs/following{/other_user}","gists_url":"https://api.github.com/users/heroiclabs/gists{/gist_id}","starred_url":"https://api.github.com/users/heroiclabs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/heroiclabs/subscriptions","organizations_url":"https://api.github.com/users/heroiclabs/orgs","repos_url":"https://api.github.com/users/heroiclabs/repos","events_url":"https://api.github.com/users/heroiclabs/events{/privacy}","received_events_url":"https://api.github.com/users/heroiclabs/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/heroiclabs/nakama","description":"Distributed server for social and realtime games and apps.","fork":false,"url":"https://api.github.com/repos/heroiclabs/nakama","forks_url":"https://api.github.com/repos/heroiclabs/nakama/forks","keys_url":"https://api.github.com/repos/heroiclabs/nakama/keys{/key_id}","collaborators_url":"https://api.github.com/repos/heroiclabs/nakama/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/heroiclabs/nakama/teams","hooks_url":"https://api.github.com/repos/heroiclabs/nakama/hooks","issue_events_url":"https://api.github.com/repos/heroiclabs/nakama/issues/events{/number}","events_url":"https://api.github.com/repos/heroiclabs/nakama/events","assignees_url":"https://api.github.com/repos/heroiclabs/nakama/assignees{/user}","branches_url":"https://api.github.com/repos/heroiclabs/nakama/branches{/branch}","tags_url":"https://api.github.com/repos/heroiclabs/nakama/tags","blobs_url":"https://api.github.com/repos/heroiclabs/nakama/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/heroiclabs/nakama/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/heroiclabs/nakama/git/refs{/sha}","trees_url":"https://api.github.com/repos/heroiclabs/nakama/git/trees{/sha}","statuses_url":"https://api.github.com/repos/heroiclabs/nakama/statuses/{sha}","languages_url":"https://api.github.com/repos/heroiclabs/nakama/languages","stargazers_url":"https://api.github.com/repos/heroiclabs/nakama/stargazers","contributors_url":"https://api.github.com/repos/heroiclabs/nakama/contributors","subscribers_url":"https://api.github.com/repos/heroiclabs/nakama/subscribers","subscription_url":"https://api.github.com/repos/heroiclabs/nakama/subscription","commits_url":"https://api.github.com/repos/heroiclabs/nakama/commits{/sha}","git_commits_url":"https://api.github.com/repos/heroiclabs/nakama/git/commits{/sha}","comments_url":"https://api.github.com/repos/heroiclabs/nakama/comments{/number}","issue_comment_url":"https://api.github.com/repos/heroiclabs/nakama/issues/comments{/number}","contents_url":"https://api.github.com/repos/heroiclabs/nakama/contents/{+path}","compare_url":"https://api.github.com/repos/heroiclabs/nakama/compare/{base}...{head}","merges_url":"https://api.github.com/repos/heroiclabs/nakama/merges","archive_url":"https://api.github.com/repos/heroiclabs/nakama/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/heroiclabs/nakama/downloads","issues_url":"https://api.github.com/repos/heroiclabs/nakama/issues{/number}","pulls_url":"https://api.github.com/repos/heroiclabs/nakama/pulls{/number}","milestones_url":"https://api.github.com/repos/heroiclabs/nakama/milestones{/number}","notifications_url":"https://api.github.com/repos/heroiclabs/nakama/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/heroiclabs/nakama/labels{/name}","releases_url":"https://api.github.com/repos/heroiclabs/nakama/releases{/id}","deployments_url":"https://api.github.com/repos/heroiclabs/nakama/deployments","created_at":"2017-01-13T17:15:12Z","updated_at":"2018-02-06T04:50:00Z","pushed_at":"2018-02-06T18:07:53Z","git_url":"git://github.com/heroiclabs/nakama.git","ssh_url":"git@github.com:heroiclabs/nakama.git","clone_url":"https://github.com/heroiclabs/nakama.git","svn_url":"https://github.com/heroiclabs/nakama","homepage":"https://heroiclabs.com","size":12269,"stargazers_count":1281,"watchers_count":1281,"language":"Go","has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":101,"mirror_url":null,"archived":false,"open_issues_count":35,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0"},"forks":101,"open_issues":35,"watchers":1281,"default_branch":"master"}},"base":{"label":"heroiclabs:devnext","ref":"devnext","sha":"aa5e46821422fb36a88a26281c9a5f41aeef1c31","user":{"login":"heroiclabs","id":12753245,"avatar_url":"https://avatars0.githubusercontent.com/u/12753245?v=4","gravatar_id":"","url":"https://api.github.com/users/heroiclabs","html_url":"https://github.com/heroiclabs","followers_url":"https://api.github.com/users/heroiclabs/followers","following_url":"https://api.github.com/users/heroiclabs/following{/other_user}","gists_url":"https://api.github.com/users/heroiclabs/gists{/gist_id}","starred_url":"https://api.github.com/users/heroiclabs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/heroiclabs/subscriptions","organizations_url":"https://api.github.com/users/heroiclabs/orgs","repos_url":"https://api.github.com/users/heroiclabs/repos","events_url":"https://api.github.com/users/heroiclabs/events{/privacy}","received_events_url":"https://api.github.com/users/heroiclabs/received_events","type":"Organization","site_admin":false},"repo":{"id":78869430,"name":"nakama","full_name":"heroiclabs/nakama","owner":{"login":"heroiclabs","id":12753245,"avatar_url":"https://avatars0.githubusercontent.com/u/12753245?v=4","gravatar_id":"","url":"https://api.github.com/users/heroiclabs","html_url":"https://github.com/heroiclabs","followers_url":"https://api.github.com/users/heroiclabs/followers","following_url":"https://api.github.com/users/heroiclabs/following{/other_user}","gists_url":"https://api.github.com/users/heroiclabs/gists{/gist_id}","starred_url":"https://api.github.com/users/heroiclabs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/heroiclabs/subscriptions","organizations_url":"https://api.github.com/users/heroiclabs/orgs","repos_url":"https://api.github.com/users/heroiclabs/repos","events_url":"https://api.github.com/users/heroiclabs/events{/privacy}","received_events_url":"https://api.github.com/users/heroiclabs/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/heroiclabs/nakama","description":"Distributed server for social and realtime games and apps.","fork":false,"url":"https://api.github.com/repos/heroiclabs/nakama","forks_url":"https://api.github.com/repos/heroiclabs/nakama/forks","keys_url":"https://api.github.com/repos/heroiclabs/nakama/keys{/key_id}","collaborators_url":"https://api.github.com/repos/heroiclabs/nakama/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/heroiclabs/nakama/teams","hooks_url":"https://api.github.com/repos/heroiclabs/nakama/hooks","issue_events_url":"https://api.github.com/repos/heroiclabs/nakama/issues/events{/number}","events_url":"https://api.github.com/repos/heroiclabs/nakama/events","assignees_url":"https://api.github.com/repos/heroiclabs/nakama/assignees{/user}","branches_url":"https://api.github.com/repos/heroiclabs/nakama/branches{/branch}","tags_url":"https://api.github.com/repos/heroiclabs/nakama/tags","blobs_url":"https://api.github.com/repos/heroiclabs/nakama/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/heroiclabs/nakama/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/heroiclabs/nakama/git/refs{/sha}","trees_url":"https://api.github.com/repos/heroiclabs/nakama/git/trees{/sha}","statuses_url":"https://api.github.com/repos/heroiclabs/nakama/statuses/{sha}","languages_url":"https://api.github.com/repos/heroiclabs/nakama/languages","stargazers_url":"https://api.github.com/repos/heroiclabs/nakama/stargazers","contributors_url":"https://api.github.com/repos/heroiclabs/nakama/contributors","subscribers_url":"https://api.github.com/repos/heroiclabs/nakama/subscribers","subscription_url":"https://api.github.com/repos/heroiclabs/nakama/subscription","commits_url":"https://api.github.com/repos/heroiclabs/nakama/commits{/sha}","git_commits_url":"https://api.github.com/repos/heroiclabs/nakama/git/commits{/sha}","comments_url":"https://api.github.com/repos/heroiclabs/nakama/comments{/number}","issue_comment_url":"https://api.github.com/repos/heroiclabs/nakama/issues/comments{/number}","contents_url":"https://api.github.com/repos/heroiclabs/nakama/contents/{+path}","compare_url":"https://api.github.com/repos/heroiclabs/nakama/compare/{base}...{head}","merges_url":"https://api.github.com/repos/heroiclabs/nakama/merges","archive_url":"https://api.github.com/repos/heroiclabs/nakama/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/heroiclabs/nakama/downloads","issues_url":"https://api.github.com/repos/heroiclabs/nakama/issues{/number}","pulls_url":"https://api.github.com/repos/heroiclabs/nakama/pulls{/number}","milestones_url":"https://api.github.com/repos/heroiclabs/nakama/milestones{/number}","notifications_url":"https://api.github.com/repos/heroiclabs/nakama/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/heroiclabs/nakama/labels{/name}","releases_url":"https://api.github.com/repos/heroiclabs/nakama/releases{/id}","deployments_url":"https://api.github.com/repos/heroiclabs/nakama/deployments","created_at":"2017-01-13T17:15:12Z","updated_at":"2018-02-06T04:50:00Z","pushed_at":"2018-02-06T18:07:53Z","git_url":"git://github.com/heroiclabs/nakama.git","ssh_url":"git@github.com:heroiclabs/nakama.git","clone_url":"https://github.com/heroiclabs/nakama.git","svn_url":"https://github.com/heroiclabs/nakama","homepage":"https://heroiclabs.com","size":12269,"stargazers_count":1281,"watchers_count":1281,"language":"Go","has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":101,"mirror_url":null,"archived":false,"open_issues_count":35,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0"},"forks":101,"open_issues":35,"watchers":1281,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/heroiclabs/nakama/pulls/145"},"html":{"href":"https://github.com/heroiclabs/nakama/pull/145"},"issue":{"href":"https://api.github.com/repos/heroiclabs/nakama/issues/145"},"comments":{"href":"https://api.github.com/repos/heroiclabs/nakama/issues/145/comments"},"review_comments":{"href":"https://api.github.com/repos/heroiclabs/nakama/pulls/145/comments"},"review_comment":{"href":"https://api.github.com/repos/heroiclabs/nakama/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/heroiclabs/nakama/pulls/145/commits"},"statuses":{"href":"https://api.github.com/repos/heroiclabs/nakama/statuses/d41b9f67e0ac8aaca9924f1a87aef34d346f0916"}},"author_association":"OWNER"}}
{"actor":{"display_login":"zyro"}}