UNPKG

actions-on-google

Version:
513 lines (487 loc) 13.8 kB
/** * Copyright 2018 Google Inc. All Rights Reserved. * * 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. */ import {AppOptions, AppHandler, ServiceBaseApp, attach} from '../../assistant'; import {JsonObject} from '../../common'; import * as common from '../../common'; import {Headers, BuiltinFrameworkMetadata} from '../../framework'; import * as Api from './api/v1'; import {google} from 'googleapis'; const encoding = 'utf8'; const homegraphWrapperDeprecationNotice = method => `SmartHomeApp.${method} Home Graph wrapper method is deprecated. Use Google APIs Node.js Client for Home Graph: https://www.npmjs.com/package/@googleapis/homegraph`; /** * @public * @deprecated Home Graph credentials are deprecated. * Use Google APIs Node.js Client for Home Graph: * https://www.npmjs.com/package/@googleapis/homegraph */ export interface SmartHomeJwt { type: 'service_account'; project_id: string; private_key_id: string; private_key: string; client_email: string; client_id: string; auth_uri: string; token_uri: string; auth_provider_x509_cert_url: string; client_x509_cert_url: string; } /** @public */ export interface SmartHomeOptions extends AppOptions { /** * An API key to use the home graph API. See * https://console.cloud.google.com/apis/api/homegraph.googleapis.com/overview * to learn more. * @public * @deprecated Home Graph credentials are deprecated. * Use Google APIs Node.js Client for Home Graph: * https://www.npmjs.com/package/@googleapis/homegraph */ key?: string; /** * A JWT (JSON Web Token) that is able to access the home graph API. * This is used for report state. See https://jwt.io/. A JWT can be * created through the Google Cloud Console: https://console.cloud.google.com/apis/credentials * @public * @deprecated Home Graph credentials are deprecated. * Use Google APIs Node.js Client for Home Graph: * https://www.npmjs.com/package/@googleapis/homegraph */ jwt?: SmartHomeJwt; } /** @public */ export interface SmartHomeHandler< TRequest extends Api.SmartHomeV1Request, TResponse extends Api.SmartHomeV1Response > { (body: TRequest, headers: Headers, framework: BuiltinFrameworkMetadata): | TResponse | Promise<TResponse>; } /** @hidden */ export interface SmartHomeHandlers { [intent: string]: SmartHomeHandler< Api.SmartHomeV1Request, Api.SmartHomeV1Response >; } /** @public */ export interface SmartHomeApp extends ServiceBaseApp { /** @hidden */ _intents: SmartHomeHandlers; /** @hidden */ _intent( intent: Api.SmartHomeV1Intents, handler: SmartHomeHandler<Api.SmartHomeV1Request, Api.SmartHomeV1Response> ): this; /** * Defines a function that will run when a SYNC request is received. * * @example * ```javascript * * const app = smarthome(); * app.onSync((body, headers) => { * return { * requestId: 'ff36...', * payload: { * ... * } * } * }) * ``` * * @param handler The function that will run for a SYNC request. It should * return a valid response or a Promise that resolves to valid response. * * @public */ onSync( handler: SmartHomeHandler< Api.SmartHomeV1SyncRequest, Api.SmartHomeV1SyncResponse > ): this; /** * Defines a function that will run when a QUERY request is received. * * @example * ```javascript * * const app = smarthome(); * app.onQuery((body, headers) => { * return { * requestId: 'ff36...', * payload: { * ... * } * } * }) * ``` * * @param handler The function that will run for a QUERY request. It should * return a valid response or a Promise that resolves to valid response. * * @public */ onQuery( handler: SmartHomeHandler< Api.SmartHomeV1QueryRequest, Api.SmartHomeV1QueryResponse > ): this; /** * Defines a function that will run when an EXECUTE request is received. * * @example * ```javascript * * const app = smarthome(); * app.onExecute((body, headers) => { * return { * requestId: 'ff36...', * payload: { * ... * } * } * }) * ``` * @param handler The function that will run for an EXECUTE request. It should * return a valid response or a Promise that resolves to valid response. * * @public */ onExecute( handler: SmartHomeHandler< Api.SmartHomeV1ExecuteRequest, Api.SmartHomeV1ExecuteResponse > ): this; /** * Defines a function that will run when a DISCONNECT request is received. * * @example * ```javascript * * const app = smarthome(); * app.onDisconnect((body, headers) => { * // User unlinked their account, stop reporting state for user * return {} * }) * ``` * @param handler The function that will run for an EXECUTE request. It should * return a valid response or a Promise that resolves to valid response. * * @public */ onDisconnect( handler: SmartHomeHandler< Api.SmartHomeV1DisconnectRequest, Api.SmartHomeV1DisconnectResponse > ): this; /** * Sends a request to the home graph to send a new SYNC request. This should * be called when a device is added or removed for a given user id. * * When calling this function, an API key needs to be provided as an option * in the constructor. See https://developers.google.com/actions/smarthome/create-app#request-sync * to learn more. * * @example * ```javascript * * const app = smarthome({ * key: "123ABC" * }); * * const addNewDevice = () => { * app.requestSync('user-123') * .then((res) => { * // Request sync was successful * }) * .catch((res) => { * // Request sync failed * }) * } * * // When request sync is called, a SYNC * // intent will be received soon after. * app.onSync(body => { * // ... * }) * ``` * * @param agentUserId The user identifier. * * @public * @deprecated Home Graph wrapper methods are deprecated. * Use Google APIs Node.js Client for Home Graph: * https://www.npmjs.com/package/@googleapis/homegraph */ requestSync(agentUserId: string): Promise<string>; /** * Reports the current state of a device or set of devices to the home graph. * This may be done if the state of the device was changed locally, like a * light turning on through a light switch. * * When calling this function, a JWT (JSON Web Token) needs to be provided * as an option in the constructor. * * @example * ```javascript * const app = smarthome({ * jwt: require('./jwt.json'); * }); * * const reportState = () => { * app.reportState({ * requestId: '123ABC', * agentUserId: 'user-123', * payload: { * devices: { * states: { * "light-123": { * on: true * } * } * } * } * }) * .then((res) => { * // Report state was successful * }) * .catch((res) => { * // Report state failed * }) * }; * ``` * * @param reportedState A payload containing a device or set of devices with their states * * @public * @deprecated Home Graph wrapper methods are deprecated. * Use Google APIs Node.js Client for Home Graph: * https://www.npmjs.com/package/@googleapis/homegraph */ reportState( reportedState: Api.SmartHomeV1ReportStateRequest ): Promise<string>; /** * @public * @deprecated Home Graph credentials are deprecated. * Use Google APIs Node.js Client for Home Graph: * https://www.npmjs.com/package/@googleapis/homegraph */ key?: string; /** * @public * @deprecated Home Graph credentials are deprecated. * Use Google APIs Node.js Client for Home Graph: * https://www.npmjs.com/package/@googleapis/homegraph */ jwt?: SmartHomeJwt; } /** @public */ export interface SmartHome { (options?: SmartHomeOptions): AppHandler & SmartHomeApp; } const makeApiCall = ( url: string, data: JsonObject, jwt?: SmartHomeJwt ): Promise<string> => { const options = { hostname: 'homegraph.googleapis.com', port: 443, path: url, method: 'POST', headers: {}, }; const apiCall = (options: JsonObject) => { if (jwt && !options.headers.Authorization) { throw new Error( 'JWT is defined but Authorization header is not defined ' + JSON.stringify(options) ); } return new Promise<string>((resolve, reject) => { const buffers: Buffer[] = []; const req = common.request(options, res => { res.on('data', d => { buffers.push(typeof d === 'string' ? Buffer.from(d, encoding) : d); }); res.on('end', () => { const apiResponse: string = Buffer.concat(buffers).toString(encoding); const apiResponseJson = JSON.parse(apiResponse); if (apiResponseJson.error && apiResponseJson.error.code >= 400) { // While the response ended, it contains an error. // In this case, this should reject the Promise. reject(apiResponse); return; } resolve(apiResponse); }); }); req.on('error', e => { reject(e); }); // Write data to request body req.write(JSON.stringify(data)); req.end(); }); }; if (jwt) { return new Promise<JsonObject>((resolve, reject) => { // For testing, we do not need to actually authorize if (jwt.client_id === 'sample-client-id') { options.headers = { Authorization: ' Bearer 1234', }; resolve(options); return; } // Generate JWT, then make the API call if provided const jwtClient = new google.auth.JWT( jwt.client_email, undefined, jwt.private_key, ['https://www.googleapis.com/auth/homegraph'], undefined ); jwtClient.authorize((err: Error, tokens: JsonObject) => { if (err) { return reject(err); } options.headers = { Authorization: ` Bearer ${tokens.access_token}`, }; resolve(options); }); }).then(options => { return apiCall(options); }); } else { return apiCall(options); } }; /** * * @example * ```javascript * * const app = smarthome({ * debug: true, * key: '<api-key>', * jwt: require('./key.json') * }); * * app.onSync((body, headers) => { * return { ... } * }); * * app.onQuery((body, headers) => { * return { ... } * }); * * app.onExecute((body, headers) => { * return { ... } * }); * * exports.smarthome = functions.https.onRequest(app); * * ``` * * @public */ export const smarthome: SmartHome = (options = {}) => attach<SmartHomeApp>( { _intents: {}, _intent(this: SmartHomeApp, intent, handler) { this._intents[intent] = handler; return this; }, onSync(this: SmartHomeApp, handler) { return this._intent('action.devices.SYNC', handler); }, onQuery(this: SmartHomeApp, handler) { return this._intent('action.devices.QUERY', handler); }, onExecute(this: SmartHomeApp, handler) { return this._intent('action.devices.EXECUTE', handler); }, onDisconnect(this: SmartHomeApp, handler) { return this._intent('action.devices.DISCONNECT', handler); }, async requestSync(this: SmartHomeApp, agentUserId) { common.warn.log(homegraphWrapperDeprecationNotice('requestSync')); if (this.jwt) { return await makeApiCall( '/v1/devices:requestSync', { agent_user_id: agentUserId, }, this.jwt ); } if (this.key) { return await makeApiCall( `/v1/devices:requestSync?key=${encodeURIComponent(this.key)}`, { agent_user_id: agentUserId, } ); } throw new Error( 'An API key was not specified. ' + 'Please visit https://console.cloud.google.com/apis/api/homegraph.googleapis.com/overview' ); }, async reportState(this: SmartHomeApp, reportedState) { common.warn.log(homegraphWrapperDeprecationNotice('reportState')); if (!this.jwt) { throw new Error( 'A JWT was not specified. ' + 'Please visit https://console.cloud.google.com/apis/credentials' ); } return await makeApiCall( '/v1/devices:reportStateAndNotification', reportedState, this.jwt ); }, key: options.key, jwt: options.jwt, async handler( this: SmartHomeApp, body: Api.SmartHomeV1Request, headers, metadata = {} ) { const {intent} = body.inputs[0]; const handler = this._intents[intent]; return { status: 200, headers: {}, body: await handler(body, headers, metadata), }; }, }, options );