All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.jasoma.stsync.ide.WebIDE.groovy Maven / Gradle / Ivy

package com.github.jasoma.stsync.ide

import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import org.jsoup.Connection
import org.jsoup.Connection.Response
import org.jsoup.Jsoup

import javax.script.ScriptException

/**
 * Encapsulates the HTTP api exposed by the SmartThings IDE. As the IDE uses a combination of JSON responses and server side rendering
 * this class does both JSON parsing and HTML scraping to recover the necessary information.
 */
class WebIDE {

    private static final String HOST = "https://graph.api.smartthings.com"
    private static final String SESSION_COOKIE_NAME = "JSESSIONID"

    def Map cookies = [:]
    def Map headers = [:]
    def loggedIn

    /**
     * Connect to a specific path on the WebIDE host. If {@link #login(java.lang.String, java.lang.String)} has been called then
     * the stored headers/cookies needed to authenticate will be set.
     *
     * @param path the path to connect to.
     * @return a configured request containing any stored headers or cookies.
     */
    def Connection connect(String path) {
        def connection = (path.startsWith("/")) ? Jsoup.connect("$HOST$path") : Jsoup.connect("$HOST/$path");
        connection.followRedirects(false)
        cookies.each { connection.cookie(it.key, it.value) }
        headers.each { connection.header(it.key, it.value) }
        return connection
    }

    /**
     * Authenticates a user with the WebIDE and saves the session data needing to authenticate subsequent requests.
     *
     * @param username the username to login with.
     * @param password the users password.
     * @throws LoginException if the authentication fails.
     */
    def void login(String username, String password) throws LoginException {
        def response = connect("j_spring_security_check")
            .data("j_username", username)
            .data("j_password", password)
            .method(Connection.Method.POST)
            .execute()

        if (response.statusCode() != 302 || !response.hasCookie(SESSION_COOKIE_NAME)) {
            throw ApiException.unexpectedResult()
        }
        if (response.header('location')?.contains('authfail')) {
            throw new LoginException()
        }
        cookies.putAll(response.cookies())
        loggedIn = true
    }

    /**
     * Fetches the list of apps for this user.
     *
     * @return a list of the apps from the ide server.
     */
    def List apps() {
        ensureLoggedIn()
        def appsPage = connect('/ide/apps').get()
        def tableData = appsPage.select('#smartapp-table tbody tr')

        if (tableData.isEmpty()) {
            throw ApiException.unexpectedResult()
        }
        return tableData.collect { SmartAppProject.fromRow(it, this) }
    }

    /**
     * Fetches all the resource descriptors for a project.
     *
     * @param projectId the id of the project to load resources for.
     * @return the resources for that project.
     */
    def AppResources downloadResourcesList(SmartAppProject project) {
        ensureLoggedIn()
        def connection = connect('/ide/app/getResourceList')
            .data('id', project.id)
            .ignoreContentType(true)
        def response = connection.execute()

        if (response.statusCode() != 200 || !response.contentType().contains('json')) {
            throw ApiException.unexpectedResult()
        }
        return AppResources.fromJson(response.body())
    }

    /**
     * Fetches the user script for an app project.
     *
     * @param project the project to get the script for.
     * @return the entire text of the script.
     */
    def String downloadScript(SmartAppProject project) {
        ensureLoggedIn()
        if (!project.hasScript()) {
            throw new IllegalStateException("No script file was found in the resources for project ${project.name} (${project.id}). " +
                    "Full resource list:\n${JsonOutput.prettyPrint(JsonOutput.toJson(project.rawResources))}")
        }

        def connection = connect('/ide/app/getCodeForResource')
            .data('id', project.id)
            .data('resourceId', project.getScriptEntry()['id'] as String)
            .data('resourceType', 'script')
            .method(Connection.Method.POST)
            .ignoreContentType(true)
        def response = connection.execute()

        if (response.statusCode() != 200 || !response.contentType().contains('groovy')) {
            throw ApiException.unexpectedResult()
        }
        return response.body();
    }

    /**
     * Upload a new version of an app script file to a project.
     *
     * @param project the project to upload the script to.
     * @param script the script contents.
     * @throws ScriptException if the script cannot be compiled on the remote server.
     */
    def void uploadScript(SmartAppProject project, String script) throws ScriptException {
        ensureLoggedIn()
        def connection = connect('/ide/app/compile')
                .data('id', project.id)
                .data('resourceId', project.getScriptEntry()['id'] as String)
                .data('resourceType', 'script')
                .data('code', script)
                .method(Connection.Method.POST)
                .ignoreContentType(true)
        def response = connection.execute()

        if (response.statusCode() != 200 || !response.contentType().concat('json')) {
            throw ApiException.unexpectedResult()
        }
        checkCompileErrors(response)
    }

    /**
     * Fetches the list of device handlers for this user.
     *
     * @return a list of device handlers from the server.
     */
    def List deviceHandlers() {
        ensureLoggedIn()
        def devicePage = connect('/ide/devices').get()
        def tableData = devicePage.select('#devicetype-table tbody tr')

        if (tableData.isEmpty()) {
            throw ApiException.unexpectedResult()
        }
        return tableData.collect { DeviceHandlerProject.fromRow(it, this) }
    }

    /**
     * Fetches the user script for a device handler project.
     *
     * @param project the project to get the script for.
     * @return the entire text of the script.
     */
    def String downloadScript(DeviceHandlerProject project) {
        ensureLoggedIn()
        def editor = connect("/ide/device/editor/${project.id}").get()
        def codeBlock = editor.select('#code')

        if (codeBlock.isEmpty()) {
            throw ApiException.unexpectedResult()
        }
        return codeBlock.first().text()
    }

    /**
     * Upload a new version of a device script file to a project.
     * 

* * WARNING: Unlike when uploading an app script the WebIDE currently will save non-compiling device handler scripts. * If compilation fails the script must be manually rolled back with another upload. * * @param project the project to upload the script to. * @param script the script contents. * @throws ScriptException if the script cannot be compiled on the remote server. */ def void uploadScript(DeviceHandlerProject project, String script) { ensureLoggedIn() def connection = connect('/ide/device/compile') .data('id', project.id) .data('code', script) .method(Connection.Method.POST) .ignoreContentType(true) def response = connection.execute() if (response.statusCode() != 200 || !response.contentType().concat('json')) { throw ApiException.unexpectedResult() } checkCompileErrors(response) } private def ensureLoggedIn() { if (!loggedIn) { throw new IllegalStateException("`login(username, password) must be called before accessing other methods on the WebIDE") } } private def checkCompileErrors(Response response) { def parser = new JsonSlurper() def results = parser.parseText(response.body()) def errors = results['errors'] if (!errors.isEmpty()) { throw new ScriptException("The script failed to compile on the remote server, errors:\n\t${errors.join('\n\t')}") } } static class LoginException extends Exception { LoginException() { super('Could not login to the WebIDE, check your username and password are correct') } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy