mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-28 13:23:07 +00:00
Jawbone Global Oauth
This commit is contained in:
@@ -17,7 +17,6 @@ definition(
|
|||||||
) {
|
) {
|
||||||
appSetting "clientId"
|
appSetting "clientId"
|
||||||
appSetting "clientSecret"
|
appSetting "clientSecret"
|
||||||
appSetting "serverUrl"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
@@ -28,16 +27,16 @@ mappings {
|
|||||||
path("/receivedToken") { action: [ POST: "receivedToken", GET: "receivedToken"] }
|
path("/receivedToken") { action: [ POST: "receivedToken", GET: "receivedToken"] }
|
||||||
path("/receiveToken") { action: [ POST: "receiveToken", GET: "receiveToken"] }
|
path("/receiveToken") { action: [ POST: "receiveToken", GET: "receiveToken"] }
|
||||||
path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] }
|
path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] }
|
||||||
|
path("/oauth/initialize") {action: [GET: "oauthInitUrl"]}
|
||||||
path("/oauth/callback") { action: [ GET: "callback" ] }
|
path("/oauth/callback") { action: [ GET: "callback" ] }
|
||||||
}
|
}
|
||||||
|
|
||||||
def getSmartThingsClientId() {
|
def getSmartThingsClientId() { return appSettings.clientId }
|
||||||
return appSettings.clientId
|
def getSmartThingsClientSecret() { return appSettings.clientSecret }
|
||||||
}
|
def getServerUrl() { return "https://graph.api.smartthings.com" }
|
||||||
|
def getShardUrl() { return getApiServerUrl() }
|
||||||
def getSmartThingsClientSecret() {
|
def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${shardUrl}" }
|
||||||
return appSettings.clientSecret
|
def buildRedirectUrl(page) { return buildActionUrl(page) }
|
||||||
}
|
|
||||||
|
|
||||||
def callback() {
|
def callback() {
|
||||||
def redirectUrl = null
|
def redirectUrl = null
|
||||||
@@ -47,7 +46,7 @@ def callback() {
|
|||||||
} else {
|
} else {
|
||||||
log.warn "No authQueryString"
|
log.warn "No authQueryString"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.JawboneAccessToken) {
|
if (state.JawboneAccessToken) {
|
||||||
log.debug "Access token already exists"
|
log.debug "Access token already exists"
|
||||||
setup()
|
setup()
|
||||||
@@ -77,20 +76,21 @@ def callback() {
|
|||||||
|
|
||||||
def authPage() {
|
def authPage() {
|
||||||
log.debug "authPage"
|
log.debug "authPage"
|
||||||
def description = null
|
def description = null
|
||||||
if (state.JawboneAccessToken == null) {
|
if (state.JawboneAccessToken == null) {
|
||||||
if (!state.accessToken) {
|
if (!state.accessToken) {
|
||||||
log.debug "About to create access token"
|
log.debug "About to create access token"
|
||||||
createAccessToken()
|
createAccessToken()
|
||||||
}
|
}
|
||||||
description = "Click to enter Jawbone Credentials"
|
description = "Click to enter Jawbone Credentials"
|
||||||
def redirectUrl = oauthInitUrl()
|
def redirectUrl = buildRedirectUrl
|
||||||
// log.debug "RedirectURL = ${redirectUrl}"
|
log.debug "RedirectURL = ${redirectUrl}"
|
||||||
return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install:false) {
|
def donebutton= state.JawboneAccessToken != null
|
||||||
section { href url:redirectUrl, style:"embedded", required:true, title:"Jawbone UP", description:description }
|
return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install: donebutton) {
|
||||||
|
section { href url:redirectUrl, style:"embedded", required:true, title:"Jawbone UP", state: hast ,description:description }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
description = "Jawbone Credentials Already Entered."
|
description = "Jawbone Credentials Already Entered."
|
||||||
return dynamicPage(name: "Credentials", title: "Jawbone UP", uninstall: true, install:true) {
|
return dynamicPage(name: "Credentials", title: "Jawbone UP", uninstall: true, install:true) {
|
||||||
section { href url: buildRedirectUrl("receivedToken"), style:"embedded", state: "complete", title:"Jawbone UP", description:description }
|
section { href url: buildRedirectUrl("receivedToken"), style:"embedded", state: "complete", title:"Jawbone UP", description:description }
|
||||||
}
|
}
|
||||||
@@ -101,8 +101,8 @@ def oauthInitUrl() {
|
|||||||
log.debug "oauthInitUrl"
|
log.debug "oauthInitUrl"
|
||||||
def stcid = getSmartThingsClientId()
|
def stcid = getSmartThingsClientId()
|
||||||
state.oauthInitState = UUID.randomUUID().toString()
|
state.oauthInitState = UUID.randomUUID().toString()
|
||||||
def oauthParams = [ response_type: "code", client_id: stcid, scope: "move_read sleep_read", redirect_uri: buildRedirectUrl("receiveToken") ]
|
def oauthParams = [ response_type: "code", client_id: stcid, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback" ]
|
||||||
return "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}"
|
redirect(location: "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}")
|
||||||
}
|
}
|
||||||
|
|
||||||
def receiveToken(redirectUrl = null) {
|
def receiveToken(redirectUrl = null) {
|
||||||
@@ -113,7 +113,7 @@ def receiveToken(redirectUrl = null) {
|
|||||||
def params = [
|
def params = [
|
||||||
uri: "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}",
|
uri: "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}",
|
||||||
]
|
]
|
||||||
httpGet(params) { response ->
|
httpGet(params) { response ->
|
||||||
log.debug "${response.data}"
|
log.debug "${response.data}"
|
||||||
log.debug "Setting access token to ${response.data.access_token}, refresh token to ${response.data.refresh_token}"
|
log.debug "Setting access token to ${response.data.access_token}, refresh token to ${response.data.refresh_token}"
|
||||||
state.JawboneAccessToken = response.data.access_token
|
state.JawboneAccessToken = response.data.access_token
|
||||||
@@ -155,7 +155,7 @@ def connectionStatus(message, redirectUrl = null) {
|
|||||||
<meta http-equiv="refresh" content="3; url=${redirectUrl}" />
|
<meta http-equiv="refresh" content="3; url=${redirectUrl}" />
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
def html = """
|
def html = """
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
@@ -231,19 +231,11 @@ String toQueryString(Map m) {
|
|||||||
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
|
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
|
||||||
}
|
}
|
||||||
|
|
||||||
def getServerUrl() { return appSettings.serverUrl ?: "https://graph.api.smartthings.com" }
|
|
||||||
|
|
||||||
def buildRedirectUrl(page) {
|
|
||||||
// log.debug "buildRedirectUrl"
|
|
||||||
// /api/token/:st_token/smartapps/installations/:id/something
|
|
||||||
return "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/${page}"
|
|
||||||
}
|
|
||||||
|
|
||||||
def validateCurrentToken() {
|
def validateCurrentToken() {
|
||||||
log.debug "validateCurrentToken"
|
log.debug "validateCurrentToken"
|
||||||
def url = "https://jawbone.com/nudge/api/v.1.1/users/@me/refreshToken"
|
def url = "https://jawbone.com/nudge/api/v.1.1/users/@me/refreshToken"
|
||||||
def requestBody = "secret=${getSmartThingsClientSecret()}"
|
def requestBody = "secret=${getSmartThingsClientSecret()}"
|
||||||
|
|
||||||
try {
|
try {
|
||||||
httpPost(uri: url, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ], body: requestBody) {response ->
|
httpPost(uri: url, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ], body: requestBody) {response ->
|
||||||
if (response.status == 200) {
|
if (response.status == 200) {
|
||||||
@@ -287,9 +279,10 @@ def validateCurrentToken() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def initialize() {
|
def initialize() {
|
||||||
def hookUrl = "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/hookCallback"
|
log.debug "Callback URL - Webhook"
|
||||||
|
def localServerUrl = getApiServerUrl()
|
||||||
|
def hookUrl = "${localServerUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/hookCallback"
|
||||||
def webhook = "https://jawbone.com/nudge/api/v.1.1/users/@me/pubsub?webhook=$hookUrl"
|
def webhook = "https://jawbone.com/nudge/api/v.1.1/users/@me/pubsub?webhook=$hookUrl"
|
||||||
log.debug "Callback URL: $webhook"
|
|
||||||
httpPost(uri: webhook, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ])
|
httpPost(uri: webhook, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,16 +292,16 @@ def setup() {
|
|||||||
|
|
||||||
if (state.JawboneAccessToken) {
|
if (state.JawboneAccessToken) {
|
||||||
def urlmember = "https://jawbone.com/nudge/api/users/@me/"
|
def urlmember = "https://jawbone.com/nudge/api/users/@me/"
|
||||||
def member = null
|
def member = null
|
||||||
httpGet(uri: urlmember, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
httpGet(uri: urlmember, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||||
member = response.data.data
|
member = response.data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
if (member) {
|
if (member) {
|
||||||
state.member = member
|
state.member = member
|
||||||
def externalId = "${app.id}.${member.xid}"
|
def externalId = "${app.id}.${member.xid}"
|
||||||
|
|
||||||
// find the appropriate child device based on my app id and the device network id
|
// find the appropriate child device based on my app id and the device network id
|
||||||
def deviceWrapper = getChildDevice("${externalId}")
|
def deviceWrapper = getChildDevice("${externalId}")
|
||||||
|
|
||||||
// invoke the generatePresenceEvent method on the child device
|
// invoke the generatePresenceEvent method on the child device
|
||||||
@@ -328,7 +321,7 @@ def setup() {
|
|||||||
|
|
||||||
def installed() {
|
def installed() {
|
||||||
enableCallback()
|
enableCallback()
|
||||||
|
|
||||||
if (!state.accessToken) {
|
if (!state.accessToken) {
|
||||||
log.debug "About to create access token"
|
log.debug "About to create access token"
|
||||||
createAccessToken()
|
createAccessToken()
|
||||||
@@ -341,7 +334,7 @@ def installed() {
|
|||||||
|
|
||||||
def updated() {
|
def updated() {
|
||||||
enableCallback()
|
enableCallback()
|
||||||
|
|
||||||
if (!state.accessToken) {
|
if (!state.accessToken) {
|
||||||
log.debug "About to create access token"
|
log.debug "About to create access token"
|
||||||
createAccessToken()
|
createAccessToken()
|
||||||
@@ -365,29 +358,29 @@ def uninstalled() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def pollChild(childDevice) {
|
def pollChild(childDevice) {
|
||||||
def member = state.member
|
def member = state.member
|
||||||
generatePollingEvents (member, childDevice)
|
generatePollingEvents (member, childDevice)
|
||||||
}
|
}
|
||||||
|
|
||||||
def generatePollingEvents (member, childDevice) {
|
def generatePollingEvents (member, childDevice) {
|
||||||
// lets figure out if the member is currently "home" (At the place)
|
// lets figure out if the member is currently "home" (At the place)
|
||||||
def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
|
def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
|
||||||
def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
|
def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
|
||||||
def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps"
|
def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps"
|
||||||
def goals = null
|
def goals = null
|
||||||
def moves = null
|
def moves = null
|
||||||
def sleeps = null
|
def sleeps = null
|
||||||
httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||||
goals = response.data.data
|
goals = response.data.data
|
||||||
}
|
}
|
||||||
httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||||
moves = response.data.data.items[0]
|
moves = response.data.data.items[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
try { // we are going to just ignore any errors
|
try { // we are going to just ignore any errors
|
||||||
log.debug "Member = ${member.first}"
|
log.debug "Member = ${member.first}"
|
||||||
log.debug "Moves Goal = ${goals.move_steps} Steps"
|
log.debug "Moves Goal = ${goals.move_steps} Steps"
|
||||||
log.debug "Moves = ${moves.details.steps} Steps"
|
log.debug "Moves = ${moves.details.steps} Steps"
|
||||||
|
|
||||||
childDevice?.sendEvent(name:"steps", value: moves.details.steps)
|
childDevice?.sendEvent(name:"steps", value: moves.details.steps)
|
||||||
childDevice?.sendEvent(name:"goal", value: goals.move_steps)
|
childDevice?.sendEvent(name:"goal", value: goals.move_steps)
|
||||||
@@ -395,29 +388,29 @@ def generatePollingEvents (member, childDevice) {
|
|||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
// eat it
|
// eat it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def generateInitialEvent (member, childDevice) {
|
def generateInitialEvent (member, childDevice) {
|
||||||
// lets figure out if the member is currently "home" (At the place)
|
// lets figure out if the member is currently "home" (At the place)
|
||||||
def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
|
def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
|
||||||
def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
|
def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
|
||||||
def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps"
|
def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps"
|
||||||
def goals = null
|
def goals = null
|
||||||
def moves = null
|
def moves = null
|
||||||
def sleeps = null
|
def sleeps = null
|
||||||
httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||||
goals = response.data.data
|
goals = response.data.data
|
||||||
}
|
}
|
||||||
httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||||
moves = response.data.data.items[0]
|
moves = response.data.data.items[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
try { // we are going to just ignore any errors
|
try { // we are going to just ignore any errors
|
||||||
log.debug "Member = ${member.first}"
|
log.debug "Member = ${member.first}"
|
||||||
log.debug "Moves Goal = ${goals.move_steps} Steps"
|
log.debug "Moves Goal = ${goals.move_steps} Steps"
|
||||||
log.debug "Moves = ${moves.details.steps} Steps"
|
log.debug "Moves = ${moves.details.steps} Steps"
|
||||||
log.debug "Sleeping state = false"
|
log.debug "Sleeping state = false"
|
||||||
childDevice?.generateSleepingEvent(false)
|
childDevice?.generateSleepingEvent(false)
|
||||||
childDevice?.sendEvent(name:"steps", value: moves.details.steps)
|
childDevice?.sendEvent(name:"steps", value: moves.details.steps)
|
||||||
childDevice?.sendEvent(name:"goal", value: goals.move_steps)
|
childDevice?.sendEvent(name:"goal", value: goals.move_steps)
|
||||||
@@ -425,27 +418,27 @@ def generateInitialEvent (member, childDevice) {
|
|||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
// eat it
|
// eat it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def setColor (steps,goal,childDevice) {
|
def setColor (steps,goal,childDevice) {
|
||||||
def result = steps * 100 / goal
|
def result = steps * 100 / goal
|
||||||
if (result < 25)
|
if (result < 25)
|
||||||
childDevice?.sendEvent(name:"steps", value: "steps", label: steps)
|
childDevice?.sendEvent(name:"steps", value: "steps", label: steps)
|
||||||
else if ((result >= 25) && (result < 50))
|
else if ((result >= 25) && (result < 50))
|
||||||
childDevice?.sendEvent(name:"steps", value: "steps1", label: steps)
|
childDevice?.sendEvent(name:"steps", value: "steps1", label: steps)
|
||||||
else if ((result >= 50) && (result < 75))
|
else if ((result >= 50) && (result < 75))
|
||||||
childDevice?.sendEvent(name:"steps", value: "steps1", label: steps)
|
childDevice?.sendEvent(name:"steps", value: "steps1", label: steps)
|
||||||
else if (result >= 75)
|
else if (result >= 75)
|
||||||
childDevice?.sendEvent(name:"steps", value: "stepsgoal", label: steps)
|
childDevice?.sendEvent(name:"steps", value: "stepsgoal", label: steps)
|
||||||
}
|
}
|
||||||
|
|
||||||
def hookEventHandler() {
|
def hookEventHandler() {
|
||||||
// log.debug "In hookEventHandler method."
|
// log.debug "In hookEventHandler method."
|
||||||
log.debug "request = ${request}"
|
log.debug "request = ${request}"
|
||||||
|
|
||||||
def json = request.JSON
|
def json = request.JSON
|
||||||
|
|
||||||
// get some stuff we need
|
// get some stuff we need
|
||||||
def userId = json.events.user_xid[0]
|
def userId = json.events.user_xid[0]
|
||||||
def json_type = json.events.type[0]
|
def json_type = json.events.type[0]
|
||||||
@@ -454,39 +447,39 @@ def hookEventHandler() {
|
|||||||
//log.debug json
|
//log.debug json
|
||||||
log.debug "Userid = ${userId}"
|
log.debug "Userid = ${userId}"
|
||||||
log.debug "Notification Type: " + json_type
|
log.debug "Notification Type: " + json_type
|
||||||
log.debug "Notification Action: " + json_action
|
log.debug "Notification Action: " + json_action
|
||||||
|
|
||||||
// find the appropriate child device based on my app id and the device network id
|
// find the appropriate child device based on my app id and the device network id
|
||||||
def externalId = "${app.id}.${userId}"
|
def externalId = "${app.id}.${userId}"
|
||||||
def childDevice = getChildDevice("${externalId}")
|
def childDevice = getChildDevice("${externalId}")
|
||||||
|
|
||||||
if (childDevice) {
|
if (childDevice) {
|
||||||
switch (json_action) {
|
switch (json_action) {
|
||||||
case "enter_sleep_mode":
|
case "enter_sleep_mode":
|
||||||
childDevice?.generateSleepingEvent(true)
|
childDevice?.generateSleepingEvent(true)
|
||||||
break
|
break
|
||||||
case "exit_sleep_mode":
|
case "exit_sleep_mode":
|
||||||
childDevice?.generateSleepingEvent(false)
|
childDevice?.generateSleepingEvent(false)
|
||||||
break
|
break
|
||||||
case "creation":
|
case "creation":
|
||||||
childDevice?.sendEvent(name:"steps", value: 0)
|
childDevice?.sendEvent(name:"steps", value: 0)
|
||||||
break
|
break
|
||||||
case "updation":
|
case "updation":
|
||||||
def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
|
def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
|
||||||
def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
|
def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
|
||||||
def goals = null
|
def goals = null
|
||||||
def moves = null
|
def moves = null
|
||||||
httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||||
goals = response.data.data
|
goals = response.data.data
|
||||||
}
|
}
|
||||||
httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||||
moves = response.data.data.items[0]
|
moves = response.data.data.items[0]
|
||||||
}
|
}
|
||||||
log.debug "Goal = ${goals.move_steps} Steps"
|
log.debug "Goal = ${goals.move_steps} Steps"
|
||||||
log.debug "Steps = ${moves.details.steps} Steps"
|
log.debug "Steps = ${moves.details.steps} Steps"
|
||||||
childDevice?.sendEvent(name:"steps", value: moves.details.steps)
|
childDevice?.sendEvent(name:"steps", value: moves.details.steps)
|
||||||
childDevice?.sendEvent(name:"goal", value: goals.move_steps)
|
childDevice?.sendEvent(name:"goal", value: goals.move_steps)
|
||||||
//setColor(moves.details.steps,goals.move_steps,childDevice)
|
//setColor(moves.details.steps,goals.move_steps,childDevice)
|
||||||
break
|
break
|
||||||
case "deletion":
|
case "deletion":
|
||||||
app.delete()
|
app.delete()
|
||||||
@@ -499,4 +492,4 @@ def hookEventHandler() {
|
|||||||
|
|
||||||
def html = """{"code":200,"message":"OK"}"""
|
def html = """{"code":200,"message":"OK"}"""
|
||||||
render contentType: 'application/json', data: html
|
render contentType: 'application/json', data: html
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user