{"action":"created","comment":{"url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/pulls/comments/213181644","pull_request_review_id":149962810,"id":213181644,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDIxMzE4MTY0NA==","diff_hunk":"@@ -0,0 +1,1004 @@\n+// MIT License\n+//\n+// Copyright 2018 Electric Imp\n+//\n+// SPDX-License-Identifier: MIT\n+//\n+// Permission is hereby granted, free of charge, to any person obtaining a copy\n+// of this software and associated documentation files (the \"Software\"), to deal\n+// in the Software without restriction, including without limitation the rights\n+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n+// copies of the Software, and to permit persons to whom the Software is\n+// furnished to do so, subject to the following conditions:\n+//\n+// The above copyright notice and this permission notice shall be\n+// included in all copies or substantial portions of the Software.\n+//\n+// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO\n+// EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES\n+// OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,\n+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n+// OTHER DEALINGS IN THE SOFTWARE.\n+\n+\n+// GoogleIoTCore is an Electric Imp agent-side library which allows your agent code to work with Google IoT Core.\n+\n+const GOOGLE_IOT_CORE_ERROR_NOT_CONNECTED           = 1000;\n+const GOOGLE_IOT_CORE_ERROR_ALREADY_CONNECTED       = 1001;\n+const GOOGLE_IOT_CORE_ERROR_OP_NOT_ALLOWED_NOW      = 1002;\n+const GOOGLE_IOT_CORE_ERROR_TOKEN_REFRESHING        = 1003;\n+const GOOGLE_IOT_CORE_ERROR_ALREADY_REGISTERED      = 1004;\n+const GOOGLE_IOT_CORE_ERROR_GENERAL                 = 1010;\n+\n+class GoogleIoTCore {\n+\n+    static VERSION = \"1.0.0\";\n+\n+    _debugEnabled   = false;\n+\n+    _projectId      = null;\n+    _cloudRegion    = null;\n+    _registryId     = null;\n+    _deviceId       = null;\n+\n+    // Metafunction to return class name when typeof <instance> is run\n+    function _typeof() {\n+        return \"GoogleIoTCore\";\n+    }\n+\n+    // Information level logger\n+    function _log(txt) {\n+        if (_debugEnabled) {\n+            server.log(\"[\" + (typeof this) + \"] \" + txt);\n+        }\n+    }\n+\n+    // Error level logger\n+    function _logError(txt) {\n+        if (_debugEnabled) {\n+            server.error(\"[\" + (typeof this) + \"] \" + txt);\n+        }\n+    }\n+\n+}\n+\n+class GoogleIoTCore.Client extends GoogleIoTCore {\n+\n+    _privateKey         = null;\n+\n+    _onConnectedCb      = null;\n+    _onDisconnectedCb   = null;\n+\n+    _options            = null;\n+    _topics             = null;\n+\n+    _transport          = null;\n+    _defaultTransport   = null;\n+\n+    // GoogleIoTCore Client class constructor.\n+    //\n+    // Parameters:\n+    //     projectId : String           Project ID.\n+    //     cloudRegion : String         Cloud region.\n+    //     registryId : String          Registry ID.\n+    //     deviceId : String            Device ID.\n+    //     privateKey : String          Private key.\n+    //     onConnected : Function       Callback called every time the client is connected.\n+    //          (optional)              The callback signature:\n+    //                                  onConnected(error), where\n+    //                                      error : Integer     0 if the connection is successful, an error code otherwise.\n+    //     onDisconnected : Function    Callback called every time the client is disconnected.\n+    //          (optional)              The callback signature:\n+    //                                  onDisconnected(error), where\n+    //                                      error : Integer     0 if the disconnection was caused by the disconnect() method,\n+    //                                                          an error code which explains a reason of the disconnection otherwise.\n+    //     options : Table              Key-value table with optional settings.\n+    //          (optional)\n+    //\n+    // Returns:                         GoogleIoTCore.Client instance created.\n+    constructor(projectId,\n+                cloudRegion,\n+                registryId,\n+                deviceId,\n+                privateKey,\n+                onConnected = null,\n+                onDisconnected = null,\n+                options = null) {\n+        const DEFAULT_SET_STATE_PARAL_REQS      = 3;\n+        const DEFAULT_PUB_TELEMETRY_PARAL_REQS  = 3;\n+\n+        // TODO: Add this as option?\n+        const TOKEN_EXPIRATION_TIME             = 3600;\n+\n+        _projectId          = projectId;\n+        _cloudRegion        = cloudRegion;\n+        _registryId         = registryId;\n+        _deviceId           = deviceId;\n+        _privateKey         = privateKey;\n+        _onConnectedCb      = onConnected;\n+        _onDisconnectedCb   = onDisconnected;\n+\n+        _options = {\n+            \"maxPendingSetStateRequests\" : DEFAULT_SET_STATE_PARAL_REQS,\n+            \"maxPendingPublishTelemetryRequests\" : DEFAULT_PUB_TELEMETRY_PARAL_REQS\n+        };\n+\n+        if (options != null) {\n+            foreach (optName, optVal in options) {\n+                _options[optName] <- optVal;\n+            }\n+        }\n+    }\n+\n+    // Registers a device in Google IoT Core.\n+    //\n+    // Parameters:\n+    //     iss : String                 JWT issuer.\n+    //     secret : String              JWT sign secret key.\n+    //     publicKey : String           Public key for a new device.\n+    //     onRegistered : Function      Callback called when the operation is completed or an error occurs.\n+    //          (optional)              The callback signature:\n+    //                                  onRegistered(error), where\n+    //                                      error : Integer     0 if the operation is completed successfully, an error code otherwise.\n+    //\n+    // Returns:                         Nothing.\n+    function register(iss, secret, publicKey, onRegistered = null) {\n+        local token = null;\n+\n+        local getDeviceDone = function (err, respBody) {\n+            if (err != 0) {\n+                _logError(\"Can't get device data. Code = \" + err + \". Body = \" + respBody);\n+                onRegistered && onRegistered(GOOGLE_IOT_CORE_ERROR_GENERAL);\n+                return;\n+            }\n+\n+            if (respBody == null) {\n+                // Device is not found\n+                _log(\"Creating device...\");\n+\n+                local created = function (err, respBody) {\n+                    if (err == 0) {\n+                        _log(\"Device has been created successfully\");\n+                        onRegistered && onRegistered(0);\n+                    } else {\n+                        _logError(\"Can't create device. Code = \" + err + \". Body = \" + respBody);\n+                        onRegistered && onRegistered(GOOGLE_IOT_CORE_ERROR_GENERAL);\n+                    }\n+                }.bindenv(this);\n+\n+                _createDevice(publicKey, token, created);\n+            } else {\n+                // Device is found. Now we should compare its public keys with our key\n+                foreach (cred in respBody.credentials) {\n+                    if (strip(publicKey) == strip(cred.publicKey.key)) {\n+                        // We've found a key equal to our key, so this is that device we wanted to register\n+                        onRegistered && onRegistered(0);\n+                        return;\n+                    }\n+                }\n+                // We've found a device but this device is not that we wanted to register\n+                onRegistered && onRegistered(GOOGLE_IOT_CORE_ERROR_ALREADY_REGISTERED);\n+            }\n+        }.bindenv(this);\n+\n+        local getTokenDone = function (err, oatoken) {\n+            if (err != 0) {\n+                onRegistered && onRegistered(GOOGLE_IOT_CORE_ERROR_GENERAL);\n+                return;\n+            }\n+            token = oatoken;\n+            _getDevice(token, getDeviceDone);\n+        }.bindenv(this);\n+\n+        _getOAuthToken(iss, secret, getTokenDone);\n+    }\n+\n+    // Opens a connection to Google IoT Core.\n+    //\n+    // Parameters:\n+    //     transport :                  Instance of GoogleIoTCore.*Transport class.\n+    //              GoogleIoTCore.*Transport\n+    //          (optional)              The callback signature:\n+    //                                  onRegistered(error), where\n+    //                                      error : Integer     0 if the operation is completed successfully, an error code otherwise.\n+    //\n+    // Returns:                         Nothing.\n+    function connect(transport = null) {\n+        if (isConnected()) {\n+            _onConnectedCb && _onConnectedCb(GOOGLE_IOT_CORE_ERROR_ALREADY_CONNECTED);\n+            return;\n+        }\n+\n+        if (transport == null) {\n+            _defaultTransport = _defaultTransport != null ? _defaultTransport : GoogleIoTCore.MqttTransport();\n+            _transport = _defaultTransport;\n+        }\n+\n+        _transport._setClient(this);\n+        _transport._setTokenMaker(_makeJwtToken.bindenv(this));\n+        _transport._setOnConnected(_onConnected.bindenv(this));\n+        _transport._setOnDisconnected(_onDisconnected.bindenv(this));\n+        _transport._setDebug(_debugEnabled);\n+        _transport._connect();\n+    }\n+\n+    // Closes the connection to Google IoT Core. Does nothing if the connection is already closed.\n+    //\n+    // Returns:                         Nothing.\n+    function disconnect() {\n+        _transport || _logError(\"Client is not connected\");\n+        _transport && _transport._disconnect();\n+    }\n+\n+    // Checks if the client is connected to Google IoT Core.\n+    //\n+    // Returns:                         Boolean: true if the client is connected, false otherwise.\n+    function isConnected() {\n+        return _transport && _transport._isConnected();\n+    }\n+\n+    // Publishes a telemetry event to Google IoT Core.\n+    //\n+    // Parameters:\n+    //     data : String or Blob        Application specific data.\n+    //     subfolder : String           The subfolder can be used as an event category or classification.\n+    //          (optional)\n+    //     onPublished : Function       Callback called when the operation is completed or an error occurs.\n+    //          (optional)              The callback signature:\n+    //                                  onPublished(data, error), where\n+    //                                      data :              The original data passed in to the publishTelemetry() method.\n+    //                                          String or Blob\n+    //                                      error : Integer     0 if the operation is completed successfully, an error code otherwise.\n+    //\n+    // Returns:                         Nothing.\n+    function publish(data, subfolder = null, onPublished = null) {\n+        if (_transport == null) {\n+            onPublished && onPublished(data, GOOGLE_IOT_CORE_ERROR_NOT_CONNECTED);\n+            return;\n+        }\n+        _transport._publish(data, subfolder, onPublished);\n+    }\n+\n+    // Enables configuration receiving from Google IoT Core.\n+    //\n+    // Parameters:\n+    //     onReceive : Function         Callback called every time a configuration is received from Google IoT Core. null disables the feature.\n+    //                                  The callback signature:\n+    //                                  onReceive(configuration), where\n+    //                                      configuration :     Configuration. An arbitrary user-defined blob.\n+    //                                          Blob\n+    //     onDone : Function            Callback called when the operation is completed or an error occurs.\n+    //          (optional)              The callback signature:\n+    //                                  onDone(error), where\n+    //                                      error : Integer     0 if the operation is completed successfully, an error code otherwise.\n+    //\n+    // Returns:                         Nothing.\n+    function enableCfgReceiving(onReceive, onDone = null) {\n+        if (_transport == null) {\n+            onDone && onDone(data, GOOGLE_IOT_CORE_ERROR_NOT_CONNECTED);\n+            return;\n+        }\n+        _transport._enableCfgReceiving(onReceive, onDone);\n+    }\n+\n+    // Reports a device state to Google IoT Core.\n+    //\n+    // Parameters:\n+    //     state : String or Blob       Device state. Application specific data.\n+    //     onReported : Function        Callback called when the operation is completed or an error occurs.\n+    //          (optional)              The callback signature:\n+    //                                  onReported(state, error), where\n+    //                                      state :             The original state passed in to the reportDeviceState() method.\n+    //                                          String or Blob\n+    //                                      error : Integer     0 if the operation is completed successfully, an error code otherwise.\n+    //\n+    // Returns:                         Nothing.\n+    function reportState(state, onReported = null) {\n+        if (_transport == null) {\n+            onReported && onReported(data, GOOGLE_IOT_CORE_ERROR_NOT_CONNECTED);\n+            return;\n+        }\n+        _transport._reportState(state, onReported);\n+    }\n+\n+    // Sets onConnected callback.\n+    //\n+    // Parameters:\n+    //     onConnected : Function       Callback called every time the client is connected.\n+    //                                  The callback signature:\n+    //                                  onConnected(error), where\n+    //                                      error : Integer     0 if the connection is successful, an error code otherwise.\n+    //\n+    // Returns:                         Nothing.\n+    function setOnConnected(callback) {\n+        _onConnectedCb = callback;\n+    }\n+\n+    // Sets onDisconnected callback.\n+    //\n+    // Parameters:\n+    //     onDisconnected : Function    Callback called every time the client is disconnected.\n+    //                                  The callback signature:\n+    //                                  onDisconnected(error), where\n+    //                                      error : Integer     0 if the disconnection was caused by the disconnect() method,\n+    //                                                          an error code which explains a reason of the disconnection otherwise.\n+    //\n+    // Returns:                         Nothing.\n+    function setOnDisconnected(callback) {\n+        _onDisconnectedCb = callback;\n+    }\n+\n+    function setDebug(value) {\n+        _debugEnabled = value;\n+        _transport && _transport._setDebug(_debugEnabled);\n+    }\n+    // -------------------- PRIVATE METHODS -------------------- //\n+\n+    function _onConnected(err) {\n+        _onConnectedCb && _onConnectedCb(err);\n+    }\n+\n+    function _onDisconnected(reason) {\n+        _onDisconnectedCb && _onDisconnectedCb(reason);\n+    }\n+\n+    function _getOAuthToken(iss, secret, callback) {\n+        local config = {\n+            \"iss\"         : iss,\n+            \"jwtSignKey\"  : secret,\n+            \"scope\"       : \"https://www.googleapis.com/auth/cloud-platform\"\n+        };\n+        // Initializing client with provided Google Firebase config\n+        local client = OAuth2.JWTProfile.Client(OAuth2.DeviceFlow.GOOGLE, config);\n+\n+        // TODO: Fix it in the OAuth lib and remove this line\n+        client._debug = false;\n+\n+        // Starting procedure of access token acquisition\n+        client.acquireAccessToken(\n+            function(resp, err) {\n+                if (err) {\n+                    _logError(\"OAuth token acquisition error: \" + err);\n+                    callback(GOOGLE_IOT_CORE_ERROR_GENERAL, null);\n+                } else {\n+                    callback(0, resp);\n+                }\n+            }.bindenv(this)\n+        );\n+    }\n+\n+    function _getDevice(oatoken, callback) {\n+        local headers = {\n+            \"authorization\" : \"Bearer \" + oatoken,\n+            \"content-type\" : \"application/json\",\n+            \"cache-control\" : \"no-cache\"\n+        };\n+\n+        local url = \"https://cloudiot.googleapis.com/v1/\";\n+        url += format(\"projects/%s/locations/%s/registries/%s/devices/%s\", _projectId, _cloudRegion, _registryId, _deviceId);\n+\n+        local req = http.get(url, headers);\n+\n+        local sent = function (resp) {\n+            if (resp.statuscode == 404) {\n+                callback(0, null);\n+            } else if (resp.statuscode == 200) {\n+                callback(0, http.jsondecode(resp.body));\n+            } else {\n+                callback(resp.statuscode, resp.body);\n+            }\n+        }.bindenv(this);\n+\n+        req.sendasync(sent);\n+    }\n+\n+    function _createDevice(publicKey, oatoken, callback) {\n+        local headers = {\n+            \"authorization\" : \"Bearer \" + oatoken,\n+            \"content-type\" : \"application/json\",\n+            \"cache-control\" : \"no-cache\"\n+        };\n+\n+        local url = \"https://cloudiot.googleapis.com/v1/\";\n+        url += format(\"projects/%s/locations/%s/registries/%s/devices\", _projectId, _cloudRegion, _registryId);\n+\n+        local deviceDesc = {\n+            \"id\": _deviceId,\n+            \"credentials\": [\n+                {\n+                    \"public_key\": {\n+                        // TODO: should we allow for another RSA format?\n+                        \"format\": \"RSA_X509_PEM\",\n+                        \"key\": publicKey\n+                    }\n+                }\n+            ]\n+        };\n+        local data = http.jsonencode(deviceDesc);\n+\n+        local req = http.post(url, headers, data);\n+\n+        local sent = function (resp) {\n+            if (resp.statuscode == 200) {\n+                callback(0, null);\n+            } else {\n+                callback(resp.statuscode, resp.body);\n+            }\n+        }.bindenv(this);\n+\n+        req.sendasync(sent);\n+    }\n+\n+    function _makeJwtToken(tokenReadyCb) {\n+        local header = http.base64encode(\"{\\\"alg\\\":\\\"RS256\\\",\\\"typ\\\":\\\"JWT\\\"}\");\n+        local curTime = time();\n+        local expTime = curTime + TOKEN_EXPIRATION_TIME;\n+        local claimset = {\n+            \"aud\"   : _projectId,\n+            \"exp\"   : expTime,\n+            \"iat\"   : curTime\n+        };\n+        local body = http.base64encode(http.jsonencode(claimset));\n+\n+        crypto.sign(crypto.RSASSA_PKCS1_SHA256, header + \".\" + body, _decodePem(_privateKey),\n+            function(err, sig) {\n+                if (err) {\n+                    server.error(err);\n+                    return;\n+                }\n+\n+                local signature = http.base64encode(sig);\n+\n+                tokenReadyCb({\n+                    \"jwtToken\" : (header + \".\" + body + \".\" + signature),\n+                    \"jwtExpiresAt\" : expTime});\n+\n+            }.bindenv(this)\n+        );\n+    }\n+\n+    // Remove the armor, concatenate the lines, and base64 decode the text.\n+    function _decodePem(str) {\n+        local lines = split(str, \"\\n\");\n+        // We really ought to iterate over the array until we find a starting line,\n+        // and then look for the matching ending line.\n+        if ((lines[0] == \"-----BEGIN PRIVATE KEY-----\"\n+                && lines[lines.len() - 1] == \"-----END PRIVATE KEY-----\") ||\n+            (lines[0] == \"-----BEGIN RSA PRIVATE KEY-----\"\n+                && lines[lines.len() - 1] == \"-----END RSA PRIVATE KEY-----\") ||\n+            (lines[0] == \"-----BEGIN PUBLIC KEY-----\"\n+                && lines[lines.len() - 1] == \"-----END PUBLIC KEY-----\"))\n+        {\n+            local all = lines.slice(1, lines.len() - 1).reduce(@(a, b) a + b);\n+            return http.base64decode(all);\n+        }\n+        return null;\n+    }\n+\n+    function _typeof() {\n+        return \"GoogleIoTCore.Client\";\n+    }\n+}\n+\n+class GoogleIoTCore.AbstractTransport extends GoogleIoTCore {\n+\n+    _options            = null;\n+\n+    _client             = null;\n+\n+    // Function. Should return a table with jwtToken and expiresAt fields\n+    _makeToken          = null;\n+\n+    _jwtToken           = null;\n+    _jwtExpiresAt       = null;\n+\n+    function _connect() {\n+        throw \"This method is not implemented\";\n+    }\n+\n+    function _disconnect() {\n+        throw \"This method is not implemented\";\n+    }\n+\n+    function _isConnected() {\n+        throw \"This method is not implemented\";\n+    }\n+\n+    function _publish(data, subfolder, onPublished) {\n+        throw \"This method is not implemented\";\n+    }\n+\n+    function _enableCfgReceiving(onReceive, onDone) {\n+        throw \"This method is not implemented\";\n+    }\n+\n+    function _reportState(state, onReported) {\n+        throw \"This method is not implemented\";\n+    }\n+\n+    function _setOnConnected(callback) {\n+        throw \"This method is not implemented\";\n+    }\n+\n+    function _setOnDisconnected(callback) {\n+        throw \"This method is not implemented\";\n+    }\n+\n+    function _setDebug(value) {\n+        _debugEnabled = value;\n+    }\n+\n+    function _setClient(client) {\n+        _client = client;\n+    }\n+\n+    function _setTokenMaker(maker) {\n+        _makeToken = maker;\n+    }\n+\n+    function _updateToken(callback) {\n+        if (_makeToken == null) {\n+            throw \"Token maker is not set\";\n+        }\n+\n+        local tokenMade = function (token) {\n+            _jwtToken = token.jwtToken;\n+            _jwtExpiresAt = token.jwtExpiresAt;\n+            callback();\n+        }.bindenv(this);\n+        _makeToken(tokenMade);\n+    }\n+}\n+\n+class GoogleIoTCore.MqttTransport extends GoogleIoTCore.AbstractTransport {\n+\n+    _stateDisconnected      = true;","path":"GoogleIoTCore.agent.nut","position":557,"original_position":557,"commit_id":"84f7b2979a36f3a774464e128b8f511a4c1c8d54","original_commit_id":"84f7b2979a36f3a774464e128b8f511a4c1c8d54","user":{"login":"ppetrosh","id":20688680,"node_id":"MDQ6VXNlcjIwNjg4Njgw","avatar_url":"https://avatars2.githubusercontent.com/u/20688680?v=4","gravatar_id":"","url":"https://api.github.com/users/ppetrosh","html_url":"https://github.com/ppetrosh","followers_url":"https://api.github.com/users/ppetrosh/followers","following_url":"https://api.github.com/users/ppetrosh/following{/other_user}","gists_url":"https://api.github.com/users/ppetrosh/gists{/gist_id}","starred_url":"https://api.github.com/users/ppetrosh/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ppetrosh/subscriptions","organizations_url":"https://api.github.com/users/ppetrosh/orgs","repos_url":"https://api.github.com/users/ppetrosh/repos","events_url":"https://api.github.com/users/ppetrosh/events{/privacy}","received_events_url":"https://api.github.com/users/ppetrosh/received_events","type":"User","site_admin":false},"body":"Do we really need `_stateConnected` and `_stateConnected`? The same question for `-ing`.","created_at":"2018-08-28T04:32:06Z","updated_at":"2018-08-28T04:32:06Z","html_url":"https://github.com/nobitlost/GoogleIoTCore/pull/1#discussion_r213181644","pull_request_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/pulls/1","author_association":"NONE","_links":{"self":{"href":"https://api.github.com/repos/nobitlost/GoogleIoTCore/pulls/comments/213181644"},"html":{"href":"https://github.com/nobitlost/GoogleIoTCore/pull/1#discussion_r213181644"},"pull_request":{"href":"https://api.github.com/repos/nobitlost/GoogleIoTCore/pulls/1"}}},"pull_request":{"url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/pulls/1","id":211181021,"node_id":"MDExOlB1bGxSZXF1ZXN0MjExMTgxMDIx","html_url":"https://github.com/nobitlost/GoogleIoTCore/pull/1","diff_url":"https://github.com/nobitlost/GoogleIoTCore/pull/1.diff","patch_url":"https://github.com/nobitlost/GoogleIoTCore/pull/1.patch","issue_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/issues/1","number":1,"state":"open","locked":false,"title":"Implementation","user":{"login":"ragrus-nbl","id":38075410,"node_id":"MDQ6VXNlcjM4MDc1NDEw","avatar_url":"https://avatars2.githubusercontent.com/u/38075410?v=4","gravatar_id":"","url":"https://api.github.com/users/ragrus-nbl","html_url":"https://github.com/ragrus-nbl","followers_url":"https://api.github.com/users/ragrus-nbl/followers","following_url":"https://api.github.com/users/ragrus-nbl/following{/other_user}","gists_url":"https://api.github.com/users/ragrus-nbl/gists{/gist_id}","starred_url":"https://api.github.com/users/ragrus-nbl/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ragrus-nbl/subscriptions","organizations_url":"https://api.github.com/users/ragrus-nbl/orgs","repos_url":"https://api.github.com/users/ragrus-nbl/repos","events_url":"https://api.github.com/users/ragrus-nbl/events{/privacy}","received_events_url":"https://api.github.com/users/ragrus-nbl/received_events","type":"User","site_admin":false},"body":"","created_at":"2018-08-27T17:50:00Z","updated_at":"2018-08-28T04:32:06Z","closed_at":null,"merged_at":null,"merge_commit_sha":"877a2a00db1fb9fecd16554d2cd8d42787ccf0be","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/pulls/1/commits","review_comments_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/pulls/1/comments","review_comment_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/pulls/comments{/number}","comments_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/issues/1/comments","statuses_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/statuses/84f7b2979a36f3a774464e128b8f511a4c1c8d54","head":{"label":"nobitlost:develop","ref":"develop","sha":"84f7b2979a36f3a774464e128b8f511a4c1c8d54","user":{"login":"nobitlost","id":25421182,"node_id":"MDEyOk9yZ2FuaXphdGlvbjI1NDIxMTgy","avatar_url":"https://avatars2.githubusercontent.com/u/25421182?v=4","gravatar_id":"","url":"https://api.github.com/users/nobitlost","html_url":"https://github.com/nobitlost","followers_url":"https://api.github.com/users/nobitlost/followers","following_url":"https://api.github.com/users/nobitlost/following{/other_user}","gists_url":"https://api.github.com/users/nobitlost/gists{/gist_id}","starred_url":"https://api.github.com/users/nobitlost/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nobitlost/subscriptions","organizations_url":"https://api.github.com/users/nobitlost/orgs","repos_url":"https://api.github.com/users/nobitlost/repos","events_url":"https://api.github.com/users/nobitlost/events{/privacy}","received_events_url":"https://api.github.com/users/nobitlost/received_events","type":"Organization","site_admin":false},"repo":{"id":144713417,"node_id":"MDEwOlJlcG9zaXRvcnkxNDQ3MTM0MTc=","name":"GoogleIoTCore","full_name":"nobitlost/GoogleIoTCore","owner":{"login":"nobitlost","id":25421182,"node_id":"MDEyOk9yZ2FuaXphdGlvbjI1NDIxMTgy","avatar_url":"https://avatars2.githubusercontent.com/u/25421182?v=4","gravatar_id":"","url":"https://api.github.com/users/nobitlost","html_url":"https://github.com/nobitlost","followers_url":"https://api.github.com/users/nobitlost/followers","following_url":"https://api.github.com/users/nobitlost/following{/other_user}","gists_url":"https://api.github.com/users/nobitlost/gists{/gist_id}","starred_url":"https://api.github.com/users/nobitlost/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nobitlost/subscriptions","organizations_url":"https://api.github.com/users/nobitlost/orgs","repos_url":"https://api.github.com/users/nobitlost/repos","events_url":"https://api.github.com/users/nobitlost/events{/privacy}","received_events_url":"https://api.github.com/users/nobitlost/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/nobitlost/GoogleIoTCore","description":null,"fork":true,"url":"https://api.github.com/repos/nobitlost/GoogleIoTCore","forks_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/forks","keys_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/keys{/key_id}","collaborators_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/teams","hooks_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/hooks","issue_events_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/issues/events{/number}","events_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/events","assignees_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/assignees{/user}","branches_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/branches{/branch}","tags_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/tags","blobs_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/git/refs{/sha}","trees_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/git/trees{/sha}","statuses_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/statuses/{sha}","languages_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/languages","stargazers_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/stargazers","contributors_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/contributors","subscribers_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/subscribers","subscription_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/subscription","commits_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/commits{/sha}","git_commits_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/git/commits{/sha}","comments_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/comments{/number}","issue_comment_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/issues/comments{/number}","contents_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/contents/{+path}","compare_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/compare/{base}...{head}","merges_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/merges","archive_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/downloads","issues_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/issues{/number}","pulls_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/pulls{/number}","milestones_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/milestones{/number}","notifications_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/labels{/name}","releases_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/releases{/id}","deployments_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/deployments","created_at":"2018-08-14T11:52:45Z","updated_at":"2018-08-14T11:52:47Z","pushed_at":"2018-08-27T17:50:01Z","git_url":"git://github.com/nobitlost/GoogleIoTCore.git","ssh_url":"git@github.com:nobitlost/GoogleIoTCore.git","clone_url":"https://github.com/nobitlost/GoogleIoTCore.git","svn_url":"https://github.com/nobitlost/GoogleIoTCore","homepage":null,"size":52,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":1,"license":null,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"base":{"label":"nobitlost:master","ref":"master","sha":"b981f23190b4c465aa9a372fbd04362e2c08f143","user":{"login":"nobitlost","id":25421182,"node_id":"MDEyOk9yZ2FuaXphdGlvbjI1NDIxMTgy","avatar_url":"https://avatars2.githubusercontent.com/u/25421182?v=4","gravatar_id":"","url":"https://api.github.com/users/nobitlost","html_url":"https://github.com/nobitlost","followers_url":"https://api.github.com/users/nobitlost/followers","following_url":"https://api.github.com/users/nobitlost/following{/other_user}","gists_url":"https://api.github.com/users/nobitlost/gists{/gist_id}","starred_url":"https://api.github.com/users/nobitlost/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nobitlost/subscriptions","organizations_url":"https://api.github.com/users/nobitlost/orgs","repos_url":"https://api.github.com/users/nobitlost/repos","events_url":"https://api.github.com/users/nobitlost/events{/privacy}","received_events_url":"https://api.github.com/users/nobitlost/received_events","type":"Organization","site_admin":false},"repo":{"id":144713417,"node_id":"MDEwOlJlcG9zaXRvcnkxNDQ3MTM0MTc=","name":"GoogleIoTCore","full_name":"nobitlost/GoogleIoTCore","owner":{"login":"nobitlost","id":25421182,"node_id":"MDEyOk9yZ2FuaXphdGlvbjI1NDIxMTgy","avatar_url":"https://avatars2.githubusercontent.com/u/25421182?v=4","gravatar_id":"","url":"https://api.github.com/users/nobitlost","html_url":"https://github.com/nobitlost","followers_url":"https://api.github.com/users/nobitlost/followers","following_url":"https://api.github.com/users/nobitlost/following{/other_user}","gists_url":"https://api.github.com/users/nobitlost/gists{/gist_id}","starred_url":"https://api.github.com/users/nobitlost/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nobitlost/subscriptions","organizations_url":"https://api.github.com/users/nobitlost/orgs","repos_url":"https://api.github.com/users/nobitlost/repos","events_url":"https://api.github.com/users/nobitlost/events{/privacy}","received_events_url":"https://api.github.com/users/nobitlost/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/nobitlost/GoogleIoTCore","description":null,"fork":true,"url":"https://api.github.com/repos/nobitlost/GoogleIoTCore","forks_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/forks","keys_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/keys{/key_id}","collaborators_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/teams","hooks_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/hooks","issue_events_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/issues/events{/number}","events_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/events","assignees_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/assignees{/user}","branches_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/branches{/branch}","tags_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/tags","blobs_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/git/refs{/sha}","trees_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/git/trees{/sha}","statuses_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/statuses/{sha}","languages_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/languages","stargazers_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/stargazers","contributors_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/contributors","subscribers_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/subscribers","subscription_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/subscription","commits_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/commits{/sha}","git_commits_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/git/commits{/sha}","comments_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/comments{/number}","issue_comment_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/issues/comments{/number}","contents_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/contents/{+path}","compare_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/compare/{base}...{head}","merges_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/merges","archive_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/downloads","issues_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/issues{/number}","pulls_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/pulls{/number}","milestones_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/milestones{/number}","notifications_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/labels{/name}","releases_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/releases{/id}","deployments_url":"https://api.github.com/repos/nobitlost/GoogleIoTCore/deployments","created_at":"2018-08-14T11:52:45Z","updated_at":"2018-08-14T11:52:47Z","pushed_at":"2018-08-27T17:50:01Z","git_url":"git://github.com/nobitlost/GoogleIoTCore.git","ssh_url":"git@github.com:nobitlost/GoogleIoTCore.git","clone_url":"https://github.com/nobitlost/GoogleIoTCore.git","svn_url":"https://github.com/nobitlost/GoogleIoTCore","homepage":null,"size":52,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":1,"license":null,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/nobitlost/GoogleIoTCore/pulls/1"},"html":{"href":"https://github.com/nobitlost/GoogleIoTCore/pull/1"},"issue":{"href":"https://api.github.com/repos/nobitlost/GoogleIoTCore/issues/1"},"comments":{"href":"https://api.github.com/repos/nobitlost/GoogleIoTCore/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/nobitlost/GoogleIoTCore/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/nobitlost/GoogleIoTCore/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/nobitlost/GoogleIoTCore/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/nobitlost/GoogleIoTCore/statuses/84f7b2979a36f3a774464e128b8f511a4c1c8d54"}},"author_association":"COLLABORATOR"}}
{"actor":{"display_login":"ppetrosh"}}