@di-zed/yandex-smart-home
Version:
The Yandex Smart Home skills for the different device types.
417 lines (416 loc) • 17 kB
JavaScript
"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();