mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-08 05:31:56 +00:00
753 lines
17 KiB
Groovy
753 lines
17 KiB
Groovy
/**
|
|
* Copyright 2015 SmartThings
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
|
* in compliance with the License. You may obtain a copy of the License at:
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing permissions and limitations under the License.
|
|
*
|
|
* Yoics Service Manager
|
|
*
|
|
* Author: SmartThings
|
|
* Date: 2013-11-19
|
|
*/
|
|
|
|
definition(
|
|
name: "Yoics (Connect)",
|
|
namespace: "smartthings",
|
|
author: "SmartThings",
|
|
description: "Connect and Control your Yoics Enabled Devices",
|
|
category: "SmartThings Internal",
|
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png",
|
|
oauth: true,
|
|
singleInstance: true
|
|
) {
|
|
appSetting "serverUrl"
|
|
}
|
|
|
|
preferences {
|
|
page(name: "auth", title: "Sign in", content: "authPage", uninstall:true)
|
|
page(name: "page2", title: "Yoics Devices", install:true, content: "listAvailableCameras")
|
|
}
|
|
|
|
|
|
mappings {
|
|
path("/foauth") {
|
|
action: [
|
|
GET: "foauth"
|
|
]
|
|
}
|
|
path("/authorize") {
|
|
action: [
|
|
POST: "authorize"
|
|
]
|
|
}
|
|
|
|
}
|
|
|
|
def authPage()
|
|
{
|
|
log.debug "authPage()"
|
|
|
|
if(!state.accessToken)
|
|
{
|
|
log.debug "about to create access token"
|
|
createAccessToken()
|
|
}
|
|
|
|
|
|
def description = "Required"
|
|
|
|
if(getAuthHashValueIsValid())
|
|
{
|
|
// TODO: Check if it's valid
|
|
if(true)
|
|
{
|
|
description = "Already saved"
|
|
}
|
|
else
|
|
{
|
|
description = "Required"
|
|
}
|
|
}
|
|
|
|
def redirectUrl = buildUrl("", "foauth")
|
|
|
|
return dynamicPage(name: "auth", title: "Yoics", nextPage:"page2") {
|
|
section("Yoics Login"){
|
|
href url:redirectUrl, style:"embedded", required:false, title:"Yoics", description:description
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
def buildUrl(String key, String endpoint="increment", Boolean absolute=true)
|
|
{
|
|
if(key) {
|
|
key = "/${key}"
|
|
}
|
|
|
|
def url = "/api/smartapps/installations/${app.id}/${endpoint}${key}?access_token=${state.accessToken}"
|
|
|
|
if (q) {
|
|
url += "q=${q}"
|
|
}
|
|
|
|
if(absolute)
|
|
{
|
|
url = serverUrl + url
|
|
}
|
|
|
|
return url
|
|
}
|
|
|
|
//Deprecated
|
|
def getServerName() {
|
|
return getServerUrl()
|
|
}
|
|
|
|
def getServerUrl() {
|
|
return appSettings.serverUrl
|
|
}
|
|
|
|
def listAvailableCameras() {
|
|
|
|
//def loginResult = forceLogin()
|
|
|
|
//if(loginResult.success)
|
|
//{
|
|
state.cameraNames = [:]
|
|
|
|
def cameras = getDeviceList().inject([:]) { c, it ->
|
|
def dni = [app.id, it.uuid].join('.')
|
|
def cameraName = it.title ?: "Yoics"
|
|
|
|
state.cameraNames[dni] = cameraName
|
|
c[dni] = cameraName
|
|
|
|
return c
|
|
}
|
|
|
|
return dynamicPage(name: "page2", title: "Yoics Devices", install:true) {
|
|
section("Select which Yoics Devices to connect"){
|
|
input(name: "cameras", title:"", type: "enum", required:false, multiple:true, metadata:[values:cameras])
|
|
}
|
|
section("Turn on which Lights when taking pictures")
|
|
{
|
|
input "switches", "capability.switch", multiple: true, required:false
|
|
}
|
|
}
|
|
//}
|
|
/*else
|
|
{
|
|
log.error "login result false"
|
|
return [errorMessage:"There was an error logging in to Dropcam"]
|
|
}*/
|
|
|
|
}
|
|
|
|
|
|
def installed() {
|
|
log.debug "Installed with settings: ${settings}"
|
|
|
|
initialize()
|
|
}
|
|
|
|
def updated() {
|
|
log.debug "Updated with settings: ${settings}"
|
|
|
|
unsubscribe()
|
|
initialize()
|
|
}
|
|
|
|
def uninstalled() {
|
|
removeChildDevices(getChildDevices())
|
|
}
|
|
|
|
def initialize() {
|
|
|
|
if(!state.suppressDelete)
|
|
{
|
|
state.suppressDelete = [:]
|
|
}
|
|
|
|
log.debug "settings: $settings"
|
|
|
|
def devices = cameras.collect { dni ->
|
|
|
|
def name = state.cameraNames[dni] ?: "Yoics Device"
|
|
|
|
def d = getChildDevice(dni)
|
|
|
|
if(!d)
|
|
{
|
|
d = addChildDevice("smartthings", "Yoics Camera", dni, null, [name:"YoicsCamera", label:name])
|
|
|
|
/* WE'LL GET PROXY ON TAKE REQUEST
|
|
def setupProxyResult = setupProxy(dni)
|
|
if(setupProxyResult.success)
|
|
{
|
|
log.debug "Setting up the proxy worked...taking image capture now?"
|
|
|
|
}
|
|
*/
|
|
|
|
//Let's not take photos on add
|
|
//d.take()
|
|
|
|
log.debug "created ${d.displayName} with id $dni"
|
|
}
|
|
else
|
|
{
|
|
log.debug "found ${d.displayName} with id $dni already exists"
|
|
}
|
|
|
|
return d
|
|
}
|
|
|
|
log.debug "created ${devices.size()} dropcams"
|
|
|
|
/* //Original Code seems to delete the dropcam that is being added */
|
|
|
|
// Delete any that are no longer in settings
|
|
def delete = getChildDevices().findAll { !cameras?.contains(it.deviceNetworkId) }
|
|
removeChildDevices(delete)
|
|
}
|
|
|
|
private removeChildDevices(delete)
|
|
{
|
|
log.debug "deleting ${delete.size()} dropcams"
|
|
delete.each {
|
|
state.suppressDelete[it.deviceNetworkId] = true
|
|
deleteChildDevice(it.deviceNetworkId)
|
|
state.suppressDelete.remove(it.deviceNetworkId)
|
|
}
|
|
}
|
|
private List getDeviceList()
|
|
{
|
|
|
|
//https://apilb.yoics.net/web/api/getdevices.ashx?token=&filter=all&whose=me&state=%20all&type=xml
|
|
|
|
def deviceListParams = [
|
|
uri: "https://apilb.yoics.net",
|
|
path: "/web/api/getdevices.ashx",
|
|
headers: ['User-Agent': validUserAgent()],
|
|
requestContentType: "application/json",
|
|
query: [token: getLoginTokenValue(), filter: "all", whose: "me", state: "all", type:"json" ]
|
|
]
|
|
|
|
log.debug "cam list via: $deviceListParams"
|
|
|
|
def multipleHtml
|
|
def singleUrl
|
|
def something
|
|
def more
|
|
|
|
def devices = []
|
|
|
|
httpGet(deviceListParams) { resp ->
|
|
|
|
log.debug "getting device list..."
|
|
|
|
something = resp.status
|
|
more = "headers: " + resp.headers.collect { "${it.name}:${it.value}" }
|
|
|
|
if(resp.status == 200)
|
|
{
|
|
def jsonString = resp.data.str
|
|
def body = new groovy.json.JsonSlurper().parseText(jsonString)
|
|
|
|
//log.debug "get devices list response: ${jsonString}"
|
|
//log.debug "get device list response: ${body}"
|
|
|
|
body.NewDataSet.Table.each { d ->
|
|
//log.debug "Addding ${d.devicealias} with address: ${d.deviceaddress}"
|
|
devices << [title: d.devicealias, uuid: d.deviceaddress] //uuid should be another name
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
// ERROR
|
|
log.error "camera list: unknown response"
|
|
}
|
|
|
|
}
|
|
|
|
log.debug "list: after getting cameras: " + [devices:devices, url:singleUrl, html:multipleHtml?.size(), something:something, more:more]
|
|
|
|
// ERROR?
|
|
return devices
|
|
}
|
|
|
|
def removeChildFromSettings(child)
|
|
{
|
|
def device = child.device
|
|
|
|
def dni = device.deviceNetworkId
|
|
log.debug "removing child device $device with dni ${dni}"
|
|
|
|
if(!state?.suppressDelete?.get(dni))
|
|
{
|
|
def newSettings = settings.cameras?.findAll { it != dni } ?: []
|
|
app.updateSetting("cameras", newSettings)
|
|
}
|
|
}
|
|
|
|
private forceLogin() {
|
|
updateAuthHash(null)
|
|
login()
|
|
}
|
|
|
|
|
|
private login() {
|
|
|
|
if(getAuthHashValueIsValid())
|
|
{
|
|
return [success:true]
|
|
}
|
|
return doLogin()
|
|
}
|
|
|
|
/*private setupProxy(dni) {
|
|
//https://apilb.yoics.net/web/api/connect.ashx?token=&deviceaddress=00:00:48:02:2A:A2:08:0E&type=xml
|
|
|
|
def address = dni?.split(/\./)?.last()
|
|
|
|
def loginParams = [
|
|
uri: "https://apilb.yoics.net",
|
|
path: "/web/api/connect.ashx",
|
|
headers: ['User-Agent': validUserAgent()],
|
|
requestContentType: "application/json",
|
|
query: [token: getLoginTokenValue(), deviceaddress:address, type:"json" ]
|
|
]
|
|
|
|
def result = [success:false]
|
|
|
|
httpGet(loginParams) { resp ->
|
|
if (resp.status == 200) //&& resp.headers.'Content-Type'.contains("application/json")
|
|
{
|
|
log.debug "login 200 json headers: " + resp.headers.collect { "${it.name}:${it.value}" }
|
|
def jsonString = resp.data.str
|
|
def body = new groovy.json.JsonSlurper().parseText(jsonString)
|
|
|
|
def proxy = body?.NewDataSet?.Table[0]?.proxy
|
|
def requested = body?.NewDataSet?.Table[0]?.requested
|
|
def expirationsec = body?.NewDataSet?.Table[0]?.expirationsec
|
|
def url = body?.NewDataSet?.Table[0]?.url
|
|
|
|
def proxyMap = [proxy:proxy, requested: requested, expirationsec:expirationsec, url: url]
|
|
|
|
if (proxy) {
|
|
//log.debug "setting ${dni} proxy to ${proxyMap}"
|
|
//updateDeviceProxy(address, proxyMap)
|
|
result.success = true
|
|
}
|
|
else
|
|
{
|
|
// ERROR: any more information we can give?
|
|
result.reason = "Bad login"
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// ERROR: any more information we can give?
|
|
result.reason = "Bad login"
|
|
}
|
|
|
|
|
|
}
|
|
|
|
return result
|
|
}*/
|
|
|
|
|
|
|
|
private doLogin(user = "", pwd = "") { //change this name
|
|
|
|
def loginParams = [
|
|
uri: "https://apilb.yoics.net",
|
|
path: "/web/api/login.ashx",
|
|
headers: ['User-Agent': validUserAgent()],
|
|
requestContentType: "application/json",
|
|
query: [key: "SmartThingsApplication", usr: username, pwd: password, apilevel: 12, type:"json" ]
|
|
]
|
|
|
|
if (user) {
|
|
loginParams.query = [key: "SmartThingsApplication", usr: user, pwd: pwd, apilevel: 12, type:"json" ]
|
|
}
|
|
|
|
def result = [success:false]
|
|
|
|
httpGet(loginParams) { resp ->
|
|
if (resp.status == 200) //&& resp.headers.'Content-Type'.contains("application/json")
|
|
{
|
|
log.debug "login 200 json headers: " + resp.headers.collect { "${it.name}:${it.value}" }
|
|
def jsonString = resp.data.str
|
|
def body = new groovy.json.JsonSlurper().parseText(jsonString)
|
|
|
|
log.debug "login response: ${jsonString}"
|
|
log.debug "login response: ${body}"
|
|
|
|
def authhash = body?.NewDataSet?.Table[0]?.authhash //.token
|
|
|
|
//this may return as well??
|
|
def token = body?.NewDataSet?.Table[0]?.token ?: null
|
|
|
|
if (authhash) {
|
|
log.debug "login setting authhash to ${authhash}"
|
|
updateAuthHash(authhash)
|
|
if (token) {
|
|
log.debug "login setting login token to ${token}"
|
|
updateLoginToken(token)
|
|
result.success = true
|
|
} else {
|
|
result.success = doLoginToken()
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// ERROR: any more information we can give?
|
|
result.reason = "Bad login"
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// ERROR: any more information we can give?
|
|
result.reason = "Bad login"
|
|
}
|
|
|
|
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
private doLoginToken() {
|
|
|
|
def loginParams = [
|
|
uri: "https://apilb.yoics.net",
|
|
path: "/web/api/login.ashx",
|
|
headers: ['User-Agent': validUserAgent()],
|
|
requestContentType: "application/json",
|
|
query: [key: "SmartThingsApplication", usr: getUserName(), auth: getAuthHashValue(), apilevel: 12, type:"json" ]
|
|
]
|
|
|
|
def result = [success:false]
|
|
|
|
httpGet(loginParams) { resp ->
|
|
if (resp.status == 200)
|
|
{
|
|
log.debug "login 200 json headers: " + resp.headers.collect { "${it.name}:${it.value}" }
|
|
|
|
def jsonString = resp.data.str
|
|
def body = new groovy.json.JsonSlurper().parseText(jsonString)
|
|
|
|
def token = body?.NewDataSet?.Table[0]?.token
|
|
|
|
if (token) {
|
|
log.debug "login setting login to $token"
|
|
updateLoginToken(token)
|
|
result.success = true
|
|
}
|
|
else
|
|
{
|
|
// ERROR: any more information we can give?
|
|
result.reason = "Bad login"
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// ERROR: any more information we can give?
|
|
result.reason = "Bad login"
|
|
}
|
|
|
|
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
def takePicture(String dni, Integer imgWidth=null)
|
|
{
|
|
|
|
//turn on any of the selected lights that are off
|
|
def offLights = switches.findAll{(it.currentValue("switch") == "off")}
|
|
log.debug offLights
|
|
offLights.collect{it.on()}
|
|
|
|
log.debug "parent.takePicture(${dni}, ${imgWidth})"
|
|
|
|
def uuid = dni?.split(/\./)?.last()
|
|
|
|
log.debug "taking picture for $uuid (${dni})"
|
|
|
|
def imageBytes
|
|
def loginRequired = false
|
|
|
|
try
|
|
{
|
|
imageBytes = doTakePicture(uuid, imgWidth)
|
|
}
|
|
catch(Exception e)
|
|
{
|
|
log.error "Exception $e trying to take a picture, attempting to login again"
|
|
loginRequired = true
|
|
}
|
|
|
|
if(loginRequired)
|
|
{
|
|
def loginResult = doLoginToken()
|
|
if(loginResult.success)
|
|
{
|
|
// try once more
|
|
imageBytes = doTakePicture(uuid, imgWidth)
|
|
}
|
|
else
|
|
{
|
|
log.error "tried to login to dropcam after failing to take a picture and failed"
|
|
}
|
|
}
|
|
|
|
//turn previously off lights to their original state
|
|
offLights.collect{it.off()}
|
|
return imageBytes
|
|
}
|
|
|
|
private doTakePicture(String uuid, Integer imgWidth)
|
|
{
|
|
imgWidth = imgWidth ?: 1280
|
|
def loginRequired = false
|
|
|
|
def proxyParams = getDeviceProxy(uuid)
|
|
if(!proxyParams.success)
|
|
{
|
|
throw new Exception("Login Required")
|
|
}
|
|
|
|
def takeParams = [
|
|
uri: "${proxyParams.uri}",
|
|
path: "${proxyParams.path}",
|
|
headers: ['User-Agent': validUserAgent()]
|
|
]
|
|
|
|
def imageBytes
|
|
|
|
httpGet(takeParams) { resp ->
|
|
|
|
if(resp.status == 403)
|
|
{
|
|
loginRequired = true
|
|
}
|
|
else if (resp.status == 200 && resp.headers.'Content-Type'.contains("image/jpeg"))
|
|
{
|
|
imageBytes = resp.data
|
|
}
|
|
else
|
|
{
|
|
log.error "unknown takePicture() response: ${resp.status} - ${resp.headers.'Content-Type'}"
|
|
}
|
|
}
|
|
|
|
if(loginRequired)
|
|
{
|
|
throw new Exception("Login Required")
|
|
}
|
|
|
|
return imageBytes
|
|
}
|
|
|
|
/////////////////////////
|
|
private Boolean getLoginTokenValueIsValid()
|
|
{
|
|
return getLoginTokenValue()
|
|
}
|
|
|
|
private updateLoginToken(String token) {
|
|
state.loginToken = token
|
|
}
|
|
|
|
private getLoginTokenValue() {
|
|
state.loginToken
|
|
}
|
|
|
|
private Boolean getAuthHashValueIsValid()
|
|
{
|
|
return getAuthHashValue()
|
|
}
|
|
|
|
private updateAuthHash(String hash) {
|
|
state.authHash = hash
|
|
}
|
|
|
|
private getAuthHashValue() {
|
|
state.authHash
|
|
}
|
|
|
|
private updateUserName(String username) {
|
|
state.username = username
|
|
}
|
|
|
|
private getUserName() {
|
|
state.username
|
|
}
|
|
|
|
/*private getDeviceProxy(dni){
|
|
//check if it exists or is not longer valid and create a new proxy here
|
|
log.debug "returning proxy ${state.proxy[dni].proxy}"
|
|
def proxy = [uri:state.proxy[dni].proxy, path:state.proxy[dni].url]
|
|
log.debug "returning proxy ${proxy}"
|
|
proxy
|
|
}*/
|
|
|
|
private updateDeviceProxy(dni, map){
|
|
if (!state.proxy) { state.proxy = [:] }
|
|
state.proxy[dni] = map
|
|
}
|
|
|
|
private getDeviceProxy(dni) {
|
|
def address = dni?.split(/\./)?.last()
|
|
|
|
def loginParams = [
|
|
uri: "https://apilb.yoics.net",
|
|
path: "/web/api/connect.ashx",
|
|
headers: ['User-Agent': validUserAgent()],
|
|
requestContentType: "application/json",
|
|
query: [token: getLoginTokenValue(), deviceaddress:address, type:"json" ]
|
|
]
|
|
|
|
def result = [success:false]
|
|
|
|
httpGet(loginParams) { resp ->
|
|
if (resp.status == 200) //&& resp.headers.'Content-Type'.contains("application/json")
|
|
{
|
|
log.debug "login 200 json headers: " + resp.headers.collect { "${it.name}:${it.value}" }
|
|
def jsonString = resp.data.str
|
|
def body = new groovy.json.JsonSlurper().parseText(jsonString)
|
|
|
|
if (body?.NewDataSet?.Table[0]?.error)
|
|
{
|
|
log.error "Attempt to get Yoics Proxy failed"
|
|
// ERROR: any more information we can give?
|
|
result.reason = body?.NewDataSet?.Table[0]?.message
|
|
}
|
|
else
|
|
{
|
|
result.uri = body?.NewDataSet?.Table[0]?.proxy
|
|
result.path = body?.NewDataSet?.Table[0]?.url
|
|
result.requested = body?.NewDataSet?.Table[0]?.requested
|
|
result.expirationsec = body?.NewDataSet?.Table[0]?.expirationsec
|
|
result.success = true
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
// ERROR: any more information we can give?
|
|
result.reason = "Bad login"
|
|
}
|
|
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
private validUserAgent() {
|
|
"curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8x zlib/1.2.5"
|
|
}
|
|
|
|
def foauth() {
|
|
def html = """<html>
|
|
<head>
|
|
<title>$inputQuery results</title>
|
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<link rel="shortcut icon" href="/static/sT2cZkBCCKJduBLfQ6NfUjZg1AiMhFK9ESNxUjjlvsk.ico" type="image/x-icon">
|
|
<link rel="apple-touch-icon" href="/static/7UIUNICQhrzmPRYK3T7j5BhAsvUIbKE8OARNI702Dw9.png">
|
|
<link rel="apple-touch-icon" sizes="114x114" href="/static/HkpqhLsUc5flOzvxrpaoyybhcCP1iRd0ogxhWFJ9vKo.png">
|
|
|
|
<script src="/static/1vXORVkZK58St3QjdbzerXZDi9MfZQ8Q3wCyumiNiep.js" type="text/javascript" ></script>
|
|
<link href="/static/ZLo6WmGLBQwvykZ4sFgJS1W8IKyGj3TKdKZXyHcBB9l.css" type="text/css" rel="stylesheet" media="screen, projection" />
|
|
<link rel="stylesheet" href="/static/sd6ug4HGJyhdTwTONDZK6Yw8VsYbyDa4qUPgLokOkTn.css" type="text/css">
|
|
|
|
</head>
|
|
<body>
|
|
|
|
<h1>
|
|
Yoics Login
|
|
</h1>
|
|
|
|
<form name="login" action="${buildUrl("", "authorize")}" method="post">
|
|
User:
|
|
<br>
|
|
<input type="text" name="user" style="height: 50px;">
|
|
<br>
|
|
<br>
|
|
Password:
|
|
<br>
|
|
<input type="password" name="password" style="height: 50px;">
|
|
|
|
<input type="submit" value="Submit">
|
|
</form>
|
|
|
|
|
|
</body>
|
|
</html>"""
|
|
|
|
render status: 200, contentType: 'text/html', data: html
|
|
}
|
|
|
|
def authorize() {
|
|
|
|
def loginResult = doLogin(params.user, params.password)
|
|
|
|
def result
|
|
if (loginResult.success) {
|
|
result = "Successful"
|
|
|
|
//save username
|
|
updateUserName(params.user)
|
|
} else {
|
|
result = "Failed"
|
|
}
|
|
|
|
def html = """<html>
|
|
<head>
|
|
<title>$inputQuery results</title>
|
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<link rel="shortcut icon" href="/static/sT2cZkBCCKJduBLfQ6NfUjZg1AiMhFK9ESNxUjjlvsk.ico" type="image/x-icon">
|
|
<link rel="apple-touch-icon" href="/static/7UIUNICQhrzmPRYK3T7j5BhAsvUIbKE8OARNI702Dw9.png">
|
|
<link rel="apple-touch-icon" sizes="114x114" href="/static/HkpqhLsUc5flOzvxrpaoyybhcCP1iRd0ogxhWFJ9vKo.png">
|
|
|
|
|
|
|
|
<script src="/static/1vXORVkZK58St3QjdbzerXZDi9MfZQ8Q3wCyumiNiep.js" type="text/javascript" ></script>
|
|
<link href="/static/ZLo6WmGLBQwvykZ4sFgJS1W8IKyGj3TKdKZXyHcBB9l.css" type="text/css" rel="stylesheet" media="screen, projection" />
|
|
<link rel="stylesheet" href="/static/sd6ug4HGJyhdTwTONDZK6Yw8VsYbyDa4qUPgLokOkTn.css" type="text/css">
|
|
<script>
|
|
function buildCmd(){
|
|
|
|
}
|
|
</script>
|
|
</head>
|
|
<body>
|
|
|
|
<h1>
|
|
Yoics Login ${result}!
|
|
</h1>
|
|
|
|
</body>
|
|
</html>"""
|
|
|
|
render status: 200, contentType: 'text/html', data: html
|
|
}
|