import { HttpRequest, HttpRequestHandler, HttpResponse, MixinProvider, PushHandler, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceDescriptors, Setting, Settings, SettingValue } from '@scrypted/sdk'; import sdk from '@scrypted/sdk'; import { SettingsMixinDeviceBase } from "../../../common/src/settings-mixin"; import { randomBytes } from 'crypto'; const allInterfaceMethods: string[] = [].concat(...Object.values(ScryptedInterfaceDescriptors).map((type: any) => type.methods)); const allInterfaceProperties: string[] = [].concat(...Object.values(ScryptedInterfaceDescriptors).map((type: any) => type.properties)); const { systemManager, endpointManager, mediaManager } = sdk; const mediaObjectMethods = [ 'takePicture', 'getVideoStream', ] class WebhookMixin extends SettingsMixinDeviceBase { async getMixinSettings(): Promise { const realDevice = systemManager.getDeviceById(this.id); return [ { title: 'Create Webhook', key: 'create', description: 'Create a Webhook for a device interface. E.g., OnOff to turn a light on or off, or Camera to retrieve an image. The created webhook will be viewable in the Console.', choices: realDevice.interfaces, } ] } async putMixinSetting(key: string, value: string | number | boolean): Promise { this.onDeviceEvent(ScryptedInterface.Settings, undefined); let token = this.storage.getItem('token'); if (!token) { token = randomBytes(8).toString('hex'); this.storage.setItem('token', token); } this.console.log(); this.console.log(); this.console.log("##################################################") const localEndpoint = await endpointManager.getPublicLocalEndpoint(this.mixinProviderNativeId); const insecureLocalEndpoint = await endpointManager.getInsecurePublicLocalEndpoint(this.mixinProviderNativeId); this.console.log('Local Base URL'); this.console.log('.\t', localEndpoint + this.id + '/' + token); this.console.log(); this.console.log('Insecure Local Base URL'); this.console.log('.\t', insecureLocalEndpoint + this.id + '/' + token); this.console.log(); // let cloudEndpoint: string; // try { // cloudEndpoint = await endpointManager.getPublicCloudEndpoint(this.mixinProviderNativeId); // } // catch (e) { // this.console.error('Unable to generate cloud endpoint. Is the @scrypted/cloud plugin installed?', e); // this.console.warn('Only local network webhooks are available.'); // } const iface = ScryptedInterfaceDescriptors[value.toString()]; if (iface.properties?.length) { this.console.log(); this.console.log('Webhook Get States:') for (const property of iface.properties) { this.console.log(`.\t/${property}`); } } if (iface.methods?.length) { this.console.log(); this.console.log('Webhook Invoke Actions:') for (const method of iface.methods) { this.console.log(`.\t/${method}`); } } this.console.log('Webhook Actions can receive parameters via a JSON array query parameter "parameters".'); this.console.log('For example:'); this.console.log(".\tcurl 'http:///endpoint/@scrypted/webhook///setBrightness?parameters=[30]'"); this.console.log("##################################################") } async maybeSendMediaObject(response: HttpResponse, value: any, method: string) { if (!mediaObjectMethods.includes(method)) { response?.send(value?.toString()); return; } const buffer = await mediaManager.convertMediaObjectToBuffer(value, 'image/jpeg'); response?.send(buffer, { headers: { 'Content-Type': 'image/jpeg', } }); } async handle(request: HttpRequest, response: HttpResponse, device: ScryptedDevice, pathSegments: string[]) { const token = pathSegments[2]; if (token !== this.storage.getItem('token')) { response?.send('Invalid Token', { code: 401, }); return; } const methodOrProperty = pathSegments[3]; if (allInterfaceMethods.includes(methodOrProperty)) { const query = new URLSearchParams(request.url.split('?')[1] || ''); let parameters = []; const p = query.get('parameters'); if (p) { parameters = JSON.parse(p); } try { const result = await device[methodOrProperty](...parameters); this.maybeSendMediaObject(response, result, methodOrProperty); } catch (e) { this.console.error('webhook action error', e); response.send('Internal Error', { code: 500, }) } } else if (allInterfaceProperties.includes(methodOrProperty)) { const value = device[methodOrProperty]; if (request.headers['accept'] && request.headers['accept'].indexOf('application/json') !== -1) { response?.send(JSON.stringify({ value }), { headers: { 'Content-Type': 'application/json', } }); } else { response?.send(value.toString()); } } else { this.console.error('Unknown method or property', methodOrProperty); response.send('Not Found', { code: 404, }) } } } class WebhookPlugin extends ScryptedDeviceBase implements Settings, MixinProvider, HttpRequestHandler, PushHandler { createdMixins = new Map(); async handle(request: HttpRequest, response?: HttpResponse) { this.console.log('received webhook', request); const relPath = request.url.substring(request.rootPath.length).split('?')[0]; const pathSegments = relPath.split('/'); const id = pathSegments[1]; const device = systemManager.getDeviceById(id); this.console.log('device', id, device.name); if (!device.mixins.includes(this.id)) { this.console.error('device does not have webhooks enabled'); response.send('Not Found', { code: 404, }) return; } if (!this.createdMixins.has(id)) { await device.getSettings(); } const mixin = this.createdMixins.get(id); mixin.handle(request, response, device, pathSegments); } onRequest(request: HttpRequest, response: HttpResponse): Promise { return this.handle(request, response); } onPush(request: HttpRequest): Promise { return this.handle(request); } async getSettings(): Promise { return [ ] } async putSetting(key: string, value: SettingValue): Promise { } async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise { const set = new Set(interfaces); set.delete(ScryptedInterface.ObjectDetection); set.delete(ScryptedInterface.DeviceDiscovery); set.delete(ScryptedInterface.DeviceCreator); set.delete(ScryptedInterface.DeviceProvider); set.delete(ScryptedInterface.MixinProvider); set.delete(ScryptedInterface.PushHandler); set.delete(ScryptedInterface.EngineIOHandler); set.delete(ScryptedInterface.HttpRequestHandler); set.delete(ScryptedInterface.Settings); if (!set.size) return; return [ ScryptedInterface.Settings, ]; } async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any; }): Promise { const ret = new WebhookMixin(mixinDevice, mixinDeviceState, { mixinDeviceInterfaces, providerNativeId: this.nativeId, group: "Webhook", groupKey: "webhook", }); this.createdMixins.set(ret.id, ret); return ret; } async releaseMixin(id: string, mixinDevice: any): Promise { this.createdMixins.delete(id); } } export default new WebhookPlugin();