UNPKG

@di-zed/yandex-smart-home

Version:

The Yandex Smart Home skills for the different device types.

417 lines (416 loc) 17 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const topicAnonUserRegistry_1 = __importDefault(require("../registry/topicAnonUserRegistry")); const userRepository_1 = __importDefault(require("../repositories/userRepository")); const configProvider_1 = __importDefault(require("../providers/configProvider")); const httpProvider_1 = __importDefault(require("../providers/httpProvider")); const redisProvider_1 = __importDefault(require("../providers/redisProvider")); const deviceService_1 = __importDefault(require("./deviceService")); const mqttService_1 = __importDefault(require("./mqttService")); const topicService_1 = __importDefault(require("./topicService")); /** * Skill Service. */ class SkillService { constructor() { /** * Temporary User Devices. * * @protected */ this.tempUserDevices = {}; /** * Temporary User State Callbacks. * * @protected */ this.tempUserStateCallbacks = {}; } /** * Yandex Callbacks initialization. * * @param topic * @param oldMessage * @param newMessage * @returns Promise<boolean> */ initYandexCallbacks(topic, oldMessage, newMessage) { return __awaiter(this, void 0, void 0, function* () { const skillId = process.env.YANDEX_APP_SKILL_ID.trim(); const skillToken = process.env.YANDEX_APP_SKILL_TOKEN.trim(); if (!skillId || !skillToken) { return false; } const topicData = yield mqttService_1.default.getTopicData(topic); if (topicData === undefined) { return false; } if (topicAnonUserRegistry_1.default.isUserAnonymous(topicData.userName)) { return false; } if (!(yield this.isCallbackStateAvailable(topicData, oldMessage, newMessage))) { return false; } let user = undefined; try { user = yield userRepository_1.default.getUserByNameOrEmail(topicData.userName); } catch (err) { topicAnonUserRegistry_1.default.markUserAsAnonymous(topicData.userName); return false; } const device = yield deviceService_1.default.getUserDeviceById(user.id, topicData.deviceId); if (device === undefined) { return false; } const updatedDevice = yield deviceService_1.default.updateUserDevice(user, device); const tempTimeoutDevices = yield this.addTempUserDevice(user, updatedDevice); return new Promise((resolve, reject) => { clearTimeout(tempTimeoutDevices.timeoutId); tempTimeoutDevices.timeoutId = setTimeout(() => { if (tempTimeoutDevices.isDeviceParameterChanged) { this.callbackDiscovery(user.id) .then((response) => { if (typeof response === 'object') { if (response.status === 'ok') { this.tempUserStateCallbacks[user.id] = true; // The State Callback will be executed later // via the Rest User controller (devices action) callback function. return resolve(true); } else { if (response.status === 'error' && response.error_code === 'UNKNOWN_USER') { topicAnonUserRegistry_1.default.markUserAsAnonymous(topicData.userName); } // console.log('ERROR! Init Yandex Callbacks, parameter changed.', { response }); return reject(response); } } else { return reject(response); } }) .catch((err) => { return reject(err); }); } else { this.tempUserStateCallbacks[user.id] = true; this.execTempUserStateCallback(user, Object.values(tempTimeoutDevices.payloadDevices)) .then((response) => { if (typeof response === 'object' && response.status === 'ok') { return resolve(true); } else { // console.log('ERROR! Init Yandex Callbacks, parameter NOT changed.', { response }); return reject(response); } }) .catch((err) => { return reject(err); }); } }, 3000); }); }); } /** * Execute temporary user state callback. * * @param user * @param devices * @returns Promise<RequestOutput | boolean> */ execTempUserStateCallback(user, devices) { return __awaiter(this, void 0, void 0, function* () { if (!this.tempUserStateCallbacks[user.id]) { return false; } if (topicAnonUserRegistry_1.default.isUserAnonymous(user.email)) { return false; } try { const response = yield this.callbackState(user.id, devices); if (typeof response === 'object') { if (response.status === 'ok') { delete this.tempUserStateCallbacks[user.id]; this.deleteTempUserDevices(user); yield this.logLatestSkillUpdate(user.email, devices); } else if (response.status === 'error' && response.error_code === 'UNKNOWN_USER') { topicAnonUserRegistry_1.default.markUserAsAnonymous(user.email); } } return response; } catch (err) { console.log('ERROR! Execute temporary user state callback.', err); return false; } }); } /** * Notification about device state change. * https://yandex.ru/dev/dialogs/smart-home/doc/en/reference-alerts/post-skill_id-callback-state * * @param userId * @param devices * @returns Promise<RequestOutput | boolean> */ callbackState(userId, devices) { return __awaiter(this, void 0, void 0, function* () { const skillId = process.env.YANDEX_APP_SKILL_ID.trim(); const skillToken = process.env.YANDEX_APP_SKILL_TOKEN.trim(); if (!skillId || !skillToken) { return false; } const payloadDevices = []; for (const device of devices) { payloadDevices.push(this.getPayloadDevice(device)); } try { const body = { ts: this.getUnixTimestamp(), payload: { user_id: String(userId), devices: payloadDevices, }, }; const response = yield httpProvider_1.default.post(`https://dialogs.yandex.net/api/v1/skills/${skillId}/callback/state`, body, { headers: { Authorization: `Bearer ${skillToken}` }, }); const callbackSkillState = configProvider_1.default.getConfigOption('callbackSkillState'); if (typeof callbackSkillState === 'function') { callbackSkillState(response, body).catch((err) => console.log('ERROR! Skill Callback State Config Method.', err)); } return response; } catch (err) { console.log('ERROR! Skill Callback State Request.', err); return false; } }); } /** * Notification about device parameter change. * https://yandex.ru/dev/dialogs/smart-home/doc/en/reference-alerts/post-skill_id-callback-discovery * * @param userId * @returns Promise<RequestOutput | boolean> */ callbackDiscovery(userId) { return __awaiter(this, void 0, void 0, function* () { const skillId = process.env.YANDEX_APP_SKILL_ID.trim(); const skillToken = process.env.YANDEX_APP_SKILL_TOKEN.trim(); if (!skillId || !skillToken) { return false; } try { const body = { ts: this.getUnixTimestamp(), payload: { user_id: String(userId), }, }; const response = yield httpProvider_1.default.post(`https://dialogs.yandex.net/api/v1/skills/${skillId}/callback/discovery`, body, { headers: { Authorization: `Bearer ${skillToken}` }, }); const callbackSkillDiscovery = configProvider_1.default.getConfigOption('callbackSkillDiscovery'); if (typeof callbackSkillDiscovery === 'function') { callbackSkillDiscovery(response, body).catch((err) => console.log('ERROR! Skill Callback Discovery Config Method.', err)); } return response; } catch (err) { console.log('ERROR! Skill Callback Discovery Request.', err); return false; } }); } /** * Get UNIX Timestamp. * * @returns number */ getUnixTimestamp() { return Math.round(+new Date() / 1000); } /** * Log the latest Skill Update for the User. * * @param email * @param devices * @returns Promise<boolean> */ logLatestSkillUpdate(email, devices) { return __awaiter(this, void 0, void 0, function* () { const redisClient = yield redisProvider_1.default.getClientAsync(); const result = yield redisClient.hSet('log_user_skill_updates', email, JSON.stringify({ devices: devices, updatedAt: this.getUnixTimestamp(), })); return result > 0; }); } /** * Get the latest logged Skill Update for the User. * * @param email * @returns Promise<LogUserSkillUpdate | undefined> */ getLatestSkillUpdate(email) { return __awaiter(this, void 0, void 0, function* () { const redisClient = yield redisProvider_1.default.getClientAsync(); const value = yield redisClient.hGet('log_user_skill_updates', email); if (value !== undefined && value !== null) { return JSON.parse(value); } return undefined; }); } /** * Is Callback State Available? * * @param topicData * @param oldMessage * @param newMessage * @returns Promise<boolean> */ isCallbackStateAvailable(topicData, oldMessage, newMessage) { return __awaiter(this, void 0, void 0, function* () { let result = false; if (topicData.topicType === 'commandTopic') { result = oldMessage !== newMessage; } else if (topicData.topicType === 'stateTopic') { result = yield this.isStateTopicChanged(oldMessage, newMessage); } const callbackIsSkillCallbackStateAvailable = configProvider_1.default.getConfigOption('callbackIsSkillCallbackStateAvailable'); if (typeof callbackIsSkillCallbackStateAvailable === 'function') { result = yield callbackIsSkillCallbackStateAvailable(topicData, oldMessage, newMessage, result); } return result; }); } /** * Check if the State Topic has been changed. * * @param oldMessage * @param newMessage * @param deviceType * @returns Promise<boolean> */ isStateTopicChanged(oldMessage_1, newMessage_1) { return __awaiter(this, arguments, void 0, function* (oldMessage, newMessage, deviceType = '') { const changes = yield topicService_1.default.getStateTopicChanges(oldMessage, newMessage, deviceType); return changes.length > 0; }); } /** * Check if the device has the wrong property with the "event" type. * * @param user * @param updatedDevice * @returns Promise<boolean> * @protected */ isDeviceParameterChanged(user, updatedDevice) { return __awaiter(this, void 0, void 0, function* () { var _a, _b, _c, _d; const latestSkillUpdate = yield this.getLatestSkillUpdate(user.email); if (latestSkillUpdate === undefined) { return true; } let result = false; for (const latestDevice of latestSkillUpdate.devices) { if (latestDevice.id !== updatedDevice.id) { continue; } if (((_a = latestDevice.capabilities) === null || _a === void 0 ? void 0 : _a.length) !== ((_b = updatedDevice.capabilities) === null || _b === void 0 ? void 0 : _b.length)) { result = true; } if (((_c = latestDevice.properties) === null || _c === void 0 ? void 0 : _c.length) !== ((_d = updatedDevice.properties) === null || _d === void 0 ? void 0 : _d.length)) { result = true; } } return result; }); } /** * Add Temporary User Device. * * @param user * @param updatedDevice * @returns Promise<TempTimeoutDevices> * @protected */ addTempUserDevice(user, updatedDevice) { return __awaiter(this, void 0, void 0, function* () { if (this.tempUserDevices[user.id] === undefined) { this.tempUserDevices[user.id] = { timeoutId: undefined, payloadDevices: {}, isDeviceParameterChanged: false, }; } this.tempUserDevices[user.id].payloadDevices[updatedDevice.id] = this.getPayloadDevice(updatedDevice); if (!this.tempUserDevices[user.id].isDeviceParameterChanged) { this.tempUserDevices[user.id].isDeviceParameterChanged = yield this.isDeviceParameterChanged(user, updatedDevice); } return this.tempUserDevices[user.id]; }); } /** * Get Payload Device. * * @param device * @returns Device * @protected */ getPayloadDevice(device) { const result = { id: device.id }; if (device.error_code) { result.error_code = device.error_code; result.error_message = device.error_message || ''; return result; } result.capabilities = []; for (const capability of device.capabilities || []) { result.capabilities.push({ type: capability.type, state: capability.state, }); } result.properties = []; for (const property of device.properties || []) { result.properties.push({ type: property.type, state: property.state, }); } return result; } /** * Delete Temporary User Devices. * * @param user * @returns void * @protected */ deleteTempUserDevices(user) { delete this.tempUserDevices[user.id]; } } exports.default = new SkillService();