UNPKG

homebridge-xbox-tv

Version:

Homebridge plugin to control Xbox game consoles.

438 lines (382 loc) • 18.6 kB
import { promises as fsPromises } from 'fs'; import EventEmitter from 'events'; import { v4 as UuIdv4 } from 'uuid'; import axios from 'axios'; import Authentication from './authentication.js'; import ImpulseGenerator from '../impulsegenerator.js'; import { WebApi } from '../constants.js'; class XboxWebApi extends EventEmitter { constructor(config) { super(); this.xboxLiveId = config.xboxLiveId; this.webApiClientId = config.webApiClientId; this.webApiClientSecret = config.webApiClientSecret; this.inputsFile = config.inputsFile; this.enableDebugMode = config.enableDebugMode; //variables this.consoleAuthorized = false; this.rmEnabled = false; const authConfig = { webApiClientId: config.webApiClientId, webApiClientSecret: config.webApiClientSecret, tokensFile: config.tokensFile } this.authentication = new Authentication(authConfig); //create impulse generator this.impulseGenerator = new ImpulseGenerator(); this.impulseGenerator.on('checkAuthorization', async () => { try { await this.checkAuthorization(); } catch (error) { this.emit('error', error); }; }).on('state', (state) => { const emitState = state ? this.emit('success', `Web Api monitoring started`) : this.emit('warn', `Web Api monitoring stopped`); }); } async checkAuthorization() { try { const data = await this.authentication.checkAuthorization(); const debug = this.enableDebugMode ? this.emit('debug', `Authorization headers: ${JSON.stringify(data.headers, null, 2)}, tokens: ${JSON.stringify(data.tokens, null, 2)}`) : false; const authorized = data.tokens?.xsts?.Token?.trim() || false; if (!authorized) { this.emit('warn', `not authorized`); return false; }; this.tokens = data.tokens; this.consoleAuthorized = true; //check xbox live data try { //headers const headers = { 'Authorization': data.headers, 'Accept-Language': 'en-US', 'x-xbl-contract-version': '4', 'x-xbl-client-name': 'XboxApp', 'x-xbl-client-type': 'UWA', 'x-xbl-client-version': '39.39.22001.0', 'skillplatform': 'RemoteManagement', 'Content-Type': 'application/json' } this.headers = headers; //create axios instance this.axiosInstance = axios.create({ method: 'GET', headers: headers }); const rmEnabled = await this.consoleStatus(); const debug1 = rmEnabled ? false : this.emit('warn', `Remote management not enabled, please check your console settings`); this.rmEnabled = rmEnabled; //await this.consolesList(); await this.installedApps(); //await this.storageDevices(); //await this.userProfile(); } catch (error) { this.emit('error', `Check xbox live data error: ${error}`); }; return true; } catch (error) { throw new Error(`Check authorization error: ${error}`); }; } async consoleStatus() { try { const url = `${WebApi.Url.Xccs}/consoles/${this.xboxLiveId}`; const getConsoleStatusData = await this.axiosInstance(url); const debug = this.enableDebugMode ? this.emit('debug', `Console status data: ${JSON.stringify(getConsoleStatusData.data, null, 2)}`) : false; //get console status const consoleStatusData = getConsoleStatusData.data; const id = consoleStatusData.id; const name = consoleStatusData.name; const locale = consoleStatusData.locale; const region = consoleStatusData.region; const consoleType = WebApi.Console.Name[consoleStatusData.consoleType]; const powerState = WebApi.Console.PowerState[consoleStatusData.powerState] === 1; // 0 - Off, 1 - On, 2 - InStandby, 3 - SystemUpdate const playbackState = WebApi.Console.PlaybackState[consoleStatusData.playbackState] === 1; // 0 - Stopped, 1 - Playng, 2 - Paused const loginState = consoleStatusData.loginState; const focusAppAumid = consoleStatusData.focusAppAumid; const isTvConfigured = consoleStatusData.isTvConfigured === true; const digitalAssistantRemoteControlEnabled = consoleStatusData.digitalAssistantRemoteControlEnabled; const consoleStreamingEnabled = consoleStatusData.consoleStreamingEnabled; const remoteManagementEnabled = consoleStatusData.remoteManagementEnabled; //emit console type this.emit('consoleStatus', consoleType); //emit restFul and mqtt this.emit('restFul', 'status', consoleStatusData); this.emit('mqtt', 'status', consoleStatusData); return remoteManagementEnabled; } catch (error) { throw new Error(`Status error: ${error}`); }; } async consolesList() { try { const url = `${WebApi.Url.Xccs}/lists/devices?queryCurrentDevice=false&includeStorageDevices=true`; const getConsolesListData = await this.axiosInstance(url); const debug = this.enableDebugMode ? this.emit('debug', `Consoles list data: ${getConsolesListData.data.result[0]}, ${getConsolesListData.data.result[0].storageDevices[0]}`) : false; //get consoles list this.consolesId = []; this.consolesName = []; this.consolesLocale = []; this.consolesRegion = []; this.consolesConsoleType = []; this.consolesPowerState = []; this.consolesDigitalAssistantRemoteControlEnabled = []; this.consolesConsoleStreamingEnabled = []; this.consolesRemoteManagementEnabled = []; this.consolesWirelessWarning = []; this.consolesOutOfHomeWarning = []; this.consolesStorageDeviceId = []; this.consolesStorageDeviceName = []; this.consolesIsDefault = []; this.consolesFreeSpaceBytes = []; this.consolesTotalSpaceBytes = []; this.consolesIsGen9Compatible = []; const consolesList = getConsolesListData.data.result; for (const console of consolesList) { const id = console.id; const name = console.name; const locale = console.locale; const region = console.region; const consoleType = console.consoleType; const powerState = WebApi.Console.PowerState[console.powerState]; // 0 - Off, 1 - On, 2 - ConnectedStandby, 3 - SystemUpdate const digitalAssistantRemoteControlEnabled = console.digitalAssistantRemoteControlEnabled; const remoteManagementEnabled = console.remoteManagementEnabled; const consoleStreamingEnabled = console.consoleStreamingEnabled; const wirelessWarning = console.wirelessWarning; const outOfHomeWarning = console.outOfHomeWarning; this.consolesId.push(id); this.consolesName.push(name); this.consolesLocale.push(locale); this.consolesRegion.push(region); this.consolesConsoleType.push(consoleType); this.consolesPowerState.push(powerState); this.consolesDigitalAssistantRemoteControlEnabled.push(digitalAssistantRemoteControlEnabled); this.consolesRemoteManagementEnabled.push(remoteManagementEnabled); this.consolesConsoleStreamingEnabled.push(consoleStreamingEnabled); this.consolesWirelessWarning.push(wirelessWarning); this.consolesOutOfHomeWarning.push(outOfHomeWarning); const consolesStorageDevices = console.storageDevices; for (const consoleStorageDevice of consolesStorageDevices) { const storageDeviceId = consoleStorageDevice.storageDeviceId; const storageDeviceName = consoleStorageDevice.storageDeviceName; const isDefault = (consoleStorageDevice.isDefault === true); const freeSpaceBytes = consoleStorageDevice.freeSpaceBytes; const totalSpaceBytes = consoleStorageDevice.totalSpaceBytes; const isGen9Compatible = consoleStorageDevice.isGen9Compatible; this.consolesStorageDeviceId.push(storageDeviceId); this.consolesStorageDeviceName.push(storageDeviceName); this.consolesIsDefault.push(isDefault); this.consolesFreeSpaceBytes.push(freeSpaceBytes); this.consolesTotalSpaceBytes.push(totalSpaceBytes); this.consolesIsGen9Compatible.push(isGen9Compatible); } } //emit restFul and mqtt this.emit('restFul', 'consoleslist', consolesList); this.emit('mqtt', 'Consoles List', consolesList); return true; } catch (error) { throw new Error(`Consoles list error: ${error}`); }; } async installedApps() { try { const url = `${WebApi.Url.Xccs}/lists/installedApps?deviceId=${this.xboxLiveId}`; const getInstalledAppsData = await this.axiosInstance(url); const debug = this.enableDebugMode ? this.emit('debug', `Get installed apps data: ${JSON.stringify(getInstalledAppsData.data.result, null, 2)}`) : false; //get installed apps const appsArray = []; const apps = getInstalledAppsData.data.result; for (const app of apps) { const oneStoreProductId = app.oneStoreProductId; const titleId = app.titleId; const aumid = app.aumid; const lastActiveTime = app.lastActiveTime; const isGame = app.isGame; const name = app.name; const contentType = app.contentType; const instanceId = app.instanceId; const storageDeviceId = app.storageDeviceId; const uniqueId = app.uniqueId; const legacyProductId = app.legacyProductId; const version = app.version; const sizeInBytes = app.sizeInBytes; const installTime = app.installTime; const updateTime = app.updateTime; const parentId = app.parentId; const inputsObj = { 'oneStoreProductId': oneStoreProductId, 'titleId': titleId, 'reference': aumid, 'isGame': isGame, 'name': name, 'contentType': contentType }; const duplicatedInput = appsArray.some(input => input.reference === aumid); const push = name && aumid && !duplicatedInput ? appsArray.push(inputsObj) : false; }; //save inputs await this.saveData(this.inputsFile, appsArray); //emit restFul and mqtt this.emit('restFul', 'apps', apps); this.emit('mqtt', 'Apps', apps); return true; } catch (error) { throw new Error(`Installed apps error: ${error}`); }; } async storageDevices() { try { const url = `${WebApi.Url.Xccs}/lists/storageDevices?deviceId=${this.xboxLiveId}`; const getStorageDevicesData = await this.axiosInstance(url); const debug = this.enableDebugMode ? this.emit('debug', `Get storage devices data: ${JSON.stringify(getStorageDevicesData.data, null, 2)}`) : false; //get console storages this.storageDeviceId = []; this.storageDeviceName = []; this.isDefault = []; this.freeSpaceBytes = []; this.totalSpaceBytes = []; this.isGen9Compatible = []; const storageDevices = getStorageDevicesData.data.result; const deviceId = getStorageDevicesData.data.deviceId; const agentUserId = getStorageDevicesData.data.agentUserId; for (const storageDevice of storageDevices) { const storageDeviceId = storageDevice.storageDeviceId; const storageDeviceName = storageDevice.storageDeviceName; const isDefault = storageDevice.isDefault; const freeSpaceBytes = storageDevice.freeSpaceBytes; const totalSpaceBytes = storageDevice.totalSpaceBytes; const isGen9Compatible = storageDevice.isGen9Compatible; this.storageDeviceId.push(storageDeviceId); this.storageDeviceName.push(storageDeviceName); this.isDefault.push(isDefault); this.freeSpaceBytes.push(freeSpaceBytes); this.totalSpaceBytes.push(totalSpaceBytes); this.isGen9Compatible.push(isGen9Compatible); }; //emit restFul and mqtt this.emit('restFul', 'storages', storageDevices); this.emit('mqtt', 'Storages', storageDevices); return true; } catch (error) { throw new Error(`storage devices error: ${error}`); }; } async userProfile() { try { const url = `https://profile.xboxlive.com/users/xuid(${this.tokens.xsts.DisplayClaims.xui[0].xid})/profile/settings?settings=GameDisplayName,GameDisplayPicRaw,Gamerscore,Gamertag`; const getUserProfileData = await this.axiosInstance(url); const debug = this.enableDebugMode ? this.emit('debug', `Get user profile data: ${JSON.stringify(getUserProfileData.data.profileUsers[0], null, 2)}, ${JSON.stringify(getUserProfileData.data.profileUsers[0].settings[0], null, 2)}`) : false //get user profiles this.userProfileId = []; this.userProfileHostId = []; this.userProfileIsSponsoredUser = []; this.userProfileSettingsId = []; this.userProfileSettingsValue = []; const profileUsers = getUserProfileData.data.profileUsers; for (const userProfile of profileUsers) { const id = userProfile.id; const hostId = userProfile.hostId; const isSponsoredUser = userProfile.isSponsoredUser; this.userProfileId.push(id); this.userProfileHostId.push(hostId); this.userProfileIsSponsoredUser.push(isSponsoredUser); const profileUsersSettings = userProfile.settings; for (const userProfileSettings of profileUsersSettings) { const id = userProfileSettings.id; const value = userProfileSettings.value; this.userProfileSettingsId.push(id); this.userProfileSettingsValue.push(value); }; }; //emit restFul and mqtt this.emit('restFul', 'profile', profileUsers); this.emit('mqtt', 'Profile', profileUsers); return true; } catch (error) { throw new Error(`User profile error: ${error}`); }; } async saveData(path, data) { try { data = JSON.stringify(data, null, 2); await fsPromises.writeFile(path, data); const debug = this.enableDebugMode ? this.emit('debug', `Saved data: ${data}`) : false; return true; } catch (error) { throw new Error(`Save data error: ${error}`); }; }; async next() { try { await this.send('Media', 'Next'); return true; } catch (error) { throw new Error(error); }; } async previous() { try { await this.send('Media', 'Previous'); return true; } catch (error) { throw new Error(error); }; } async pause() { try { await this.send('Media', 'Pause'); return true; } catch (error) { throw new Error(error); }; } async play() { try { await this.send('Media', 'Play'); return true; } catch (error) { throw new Error(error); }; } async goBack() { try { await this.send('Shell', 'GoBack'); return true; } catch (error) { throw new Error(error); }; } async send(commandType, command, payload) { if (!this.consoleAuthorized || !this.rmEnabled) { this.emit('warn', `not authorized or remote management not enabled`); return; }; const sessionid = UuIdv4(); const params = payload ?? []; const postParams = { "destination": 'Xbox', "type": commandType, "command": command, "sessionId": sessionid, "sourceId": 'com.microsoft.smartglass', "parameters": params, "linkedXboxId": this.xboxLiveId } const debug = this.enableDebugMode ? this.emit('debug', `send, type: ${commandType}, command: ${command}, params: ${params}`) : false; try { const stringifyPostParam = JSON.stringify(postParams); this.headers['Content-Length'] = stringifyPostParam.length; const headers = { headers: this.headers } const response = await axios.post(`${WebApi.Url.Xccs}/commands`, postParams, headers); const debug1 = this.enableDebugMode ? this.emit('debug', `send command, result: ${JSON.stringify(response.data, null, 2)}`) : false; return true; } catch (error) { throw new Error(`send command type: ${commandType}, command: ${command}, params: ${params}, error: ${error}`); }; } } export default XboxWebApi;