st-schema
Version:
SmartThings Schema for C2C integration
301 lines (259 loc) • 8.84 kB
JavaScript
'use strict';
const DiscoveryResponse = require("./discovery/DiscoveryResponse");
const StateRefreshResponse = require("./state/StateRefreshResponse");
const CommandResponse = require("./state/CommandResponse");
const AccessTokenRequest = require("./callbacks/AccessTokenRequest");
const STBase = require('./STBase');
const GlobalErrorTypes = require('./errors/global-error-types');
const clientId = Symbol('private)');
const clientSecret = Symbol('private)');
const discoveryHandler = Symbol('private)');
const stateRefreshHandler = Symbol('private)');
const commandHandler = Symbol('private)');
const callbackTokenRequestHandler = Symbol('private)');
const callbackAccessHandler = Symbol('private)');
const integrationDeletedHandler = Symbol('private)');
const interactionResultHandler = Symbol('private)');
const enableEventLogging = Symbol('private');
const eventLoggingSpace = Symbol('private');
const handleCallback = Symbol('private');
module.exports = class SchemaConnector {
constructor(options = {}) {
this[clientId] = options.clientId;
this[clientSecret] = options.clientSecret;
this[discoveryHandler] = ((accessToken, response, data) => {
console.log('discoverDevices not defined')
});
this[stateRefreshHandler] = ((accessToken, response, data) => {
console.log('stateRefreshHandler not defined')
});
this[commandHandler] = ((accessToken, response, devices, data) => {
console.log('commandHandler not defined')
});
this[callbackAccessHandler] = null;
this[callbackTokenRequestHandler] = (async (clientId, clientSecret, body) => {
const tokenRequest = new AccessTokenRequest(
clientId,
clientSecret,
body.headers.requestId
);
return await tokenRequest.getCallbackToken(
body.callbackUrls.oauthToken,
body.callbackAuthentication.code
);
});
this[integrationDeletedHandler] = ((accessToken, data) => {
console.log('integrationDeletedHandler not defined')
});
this[interactionResultHandler] = ((accessToken, data) => {
if (!this[enableEventLogging]) {
console.log(`INTERACTION RESULT: ${JSON.stringify(data)}`);
}
})
}
/**
* Set your smartapp automation's client id. Cannot be
* acquired until your app has been created through the
* Developer Workspace.
* @param {String} id
* @returns {SchemaConnector} SchemaConnector instance
*/
clientId(id) {
this[clientId] = id;
return this
}
/**
* Set your smartapp automation's client secret. Cannot be
* acquired until your app has been created through the
* Developer Workspace. This secret should never be shared
* or committed into a public repository.
* @param {String} secret
* @returns {SchemaConnector} SmartApp instance
*/
clientSecret(secret) {
this[clientSecret] = secret;
return this
}
/**
* Sets the discovery request handler
*/
discoveryHandler(callback) {
this[discoveryHandler] = callback;
return this
}
/**
* Sets the state refresh request handler
*/
stateRefreshHandler(callback) {
this[stateRefreshHandler] = callback;
return this
}
/**
* Sets the command request handler
*/
commandHandler(callback) {
this[commandHandler] = callback;
return this
}
/**
* Sets the handlers called after callback access is granted
*/
callbackAccessHandler(callback) {
this[callbackAccessHandler] = callback;
return this
}
/**
* Overrides the built in handler that requests callback access tokens.
* That handler is expected to return the response of the callback token request.
*/
callbackTokenRequestHandler(callback) {
this[callbackTokenRequestHandler] = callback
return this
}
/**
* Sets integration deleted handler
*/
integrationDeletedHandler(callback) {
this[integrationDeletedHandler] = callback;
return this
}
/**
* Sets interaction result handler
*/
interactionResultHandler(callback) {
this[interactionResultHandler] = callback;
return this
}
/**
* Provide a custom context store used for storing in-flight credentials
* for each installed instance of the app.
*
* @param {*} value
* @example Use the AWS DynamoDB plugin
* smartapp.contextStore(new DynamoDBSchemaCallbackStore('aws-region', 'app-table-name'))
* @example
* // Use Firebase Cloud Firestore
* smartapp.contextStore(new FirestoreDBContextStore(firebaseServiceAccount, 'app-table-name'))
* @returns {SchemaConnector} SmartApp instance
*/
callbackStore(value) {
this[contextStore] = value;
return this
}
enableEventLogging(jsonSpace = null, enableEvents = true) {
this[enableEventLogging] = enableEvents;
this[eventLoggingSpace] = jsonSpace;
return this
}
/**
* Use with an AWS Lambda function. No signature verification is required.
*
* @param {*} event
* @param {*} context
*/
async handleLambdaCallback(event, context) {
try {
const response = await this[handleCallback](event);
if (response.isError()) {
return context.succeed(response)
}
else {
return context.succeed(response);
}
}
catch (err) {
console.log("ERROR: %s", err.stack || err);
return context.fail(err)
}
}
/**
* Use with a standard HTTP webhook endpoint app. Signature verification is required.
*
* @param {*} req
* @param {*} res
*/
async handleHttpCallback(req, res) {
try {
const response = await this[handleCallback](req.body);
if (response.isError()) {
console.log("ERROR: %j", response);
res.status(200).send(response)
}
else {
res.send(response)
}
}
catch (err) {
console.log("ERROR: %s", err.stack || err);
res.status(500).send(err)
}
}
async handleCallback(body) {
try {
return await this[handleCallback](body)
}
catch (err) {
return err;
}
}
async [handleCallback](body) {
if (this[enableEventLogging]) {
console.log(`REQUEST ${JSON.stringify(body, null, this[eventLoggingSpace])}`)
}
if (!body.headers) {
return new STBase().setError(
`Invalid ST Schema request. No 'headers' field present.`,
GlobalErrorTypes.BAD_REQUEST);
}
if (!body.authentication) {
return new STBase().setError(
`Invalid ST Schema request. No 'authentication' field present.`,
GlobalErrorTypes.BAD_REQUEST);
}
let response;
switch (body.headers.interactionType) {
case "discoveryRequest":
response = new DiscoveryResponse(body.headers.requestId);
await this[discoveryHandler](body.authentication.token, response, body);
break;
case "commandRequest":
response = new CommandResponse(body.headers.requestId);
await this[commandHandler](body.authentication.token, response, body.devices, body);
break;
case "stateRefreshRequest":
response = new StateRefreshResponse(body.headers.requestId);
await this[stateRefreshHandler](body.authentication.token, response, body);
break;
case "grantCallbackAccess":
response = new STBase(body.headers.interactionType, body.headers.requestId);
if (this[callbackAccessHandler]) {
if (body.callbackAuthentication.clientId === this[clientId] ) {
const tokenResponse = await this[callbackTokenRequestHandler](this[clientId], this[clientSecret], body)
await this[callbackAccessHandler](body.authentication.token, tokenResponse.callbackAuthentication, body.callbackUrls, body)
}
else {
response = new STBase(body.headers.interactionType, body.headers.requestId)
.setError(`Client ID ${body.callbackAuthentication.clientId} is invalid`, GlobalErrorTypes.INVALID_CLIENT);
}
}
break;
case "integrationDeleted":
response = new STBase(body.headers.interactionType, body.headers.requestId);
await this[integrationDeletedHandler](body.authentication.token, body);
break;
case "interactionResult":
response = new STBase(body.headers.interactionType, body.headers.requestId);
await this[interactionResultHandler](body.authentication.token, body);
break;
default:
response = new STBase(body.headers.interactionType, body.headers.requestId).setError(
`Unsupported interactionType: '${body.headers.interactionType}'`,
GlobalErrorTypes.INVALID_INTERACTION_TYPE);
break;
}
if (this[enableEventLogging]) {
console.log(`RESPONSE ${JSON.stringify(response, null, this[eventLoggingSpace])}`)
}
return response;
}
};