diff --git a/plugins/chromecast/src/main.ts b/plugins/chromecast/src/main.ts index b7e092646..a23cd0181 100644 --- a/plugins/chromecast/src/main.ts +++ b/plugins/chromecast/src/main.ts @@ -240,9 +240,7 @@ class CastDevice extends ScryptedDeviceBase implements MediaPlayer, Refresh, Eng reject: any; }; - async onConnection(request: HttpRequest, webSocketUrl: string) { - const ws = new WebSocket(webSocketUrl); - + async onConnection(request: HttpRequest, ws: WebSocket) { ws.onmessage = async (message) => { const json = JSON.parse(message.data as string); const { token } = json; diff --git a/plugins/core/src/main.ts b/plugins/core/src/main.ts index f610e9ce7..787968d03 100644 --- a/plugins/core/src/main.ts +++ b/plugins/core/src/main.ts @@ -129,6 +129,9 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng return this.aggregateCore; } + async releaseDevice(id: string, nativeId: string, device: any): Promise { + } + checkEngineIoEndpoint(request: HttpRequest, name: string) { const check = `/endpoint/@scrypted/core/engine.io/${name}/`; if (!request.url.startsWith(check)) @@ -154,9 +157,7 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng return true; } - async onConnection(request: HttpRequest, webSocketUrl: string): Promise { - const ws = new WebSocket(webSocketUrl); - + async onConnection(request: HttpRequest, ws: WebSocket): Promise { if (await this.checkService(request, ws, 'console') || await this.checkService(request, ws, 'repl')) { return; } diff --git a/plugins/google-home/src/main.ts b/plugins/google-home/src/main.ts index 5a495a306..aa76e395c 100644 --- a/plugins/google-home/src/main.ts +++ b/plugins/google-home/src/main.ts @@ -194,9 +194,7 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin this.throttleSync(); } - async onConnection(request: HttpRequest, webSocketUrl: string) { - const ws = new WebSocket(webSocketUrl); - + async onConnection(request: HttpRequest, ws: WebSocket) { ws.onmessage = async (message) => { const json = JSON.parse(message.data as string); const { token } = json; diff --git a/plugins/webrtc/src/main.ts b/plugins/webrtc/src/main.ts index ba93ea6d2..2c4098be7 100644 --- a/plugins/webrtc/src/main.ts +++ b/plugins/webrtc/src/main.ts @@ -4,7 +4,7 @@ import { Deferred } from '@scrypted/common/src/deferred'; import { listenZeroSingleClient } from '@scrypted/common/src/listen-cluster'; import { createBrowserSignalingSession } from "@scrypted/common/src/rtc-connect"; import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from '@scrypted/common/src/settings-mixin'; -import sdk, { BufferConverter, BufferConvertorOptions, DeviceCreator, DeviceCreatorSettings, DeviceProvider, FFmpegInput, HttpRequest, Intercom, MediaObject, MixinProvider, RequestMediaStream, RequestMediaStreamOptions, ResponseMediaStreamOptions, RTCSessionControl, RTCSignalingChannel, RTCSignalingClient, RTCSignalingSession, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera } from '@scrypted/sdk'; +import sdk, { BufferConverter, BufferConvertorOptions, ConnectOptions, DeviceCreator, DeviceCreatorSettings, DeviceProvider, FFmpegInput, HttpRequest, Intercom, MediaObject, MixinProvider, RequestMediaStream, RequestMediaStreamOptions, ResponseMediaStreamOptions, RTCSessionControl, RTCSignalingChannel, RTCSignalingClient, RTCSignalingSession, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera } from '@scrypted/sdk'; import { StorageSettings } from '@scrypted/sdk/storage-settings'; import crypto from 'crypto'; import net from 'net'; @@ -402,7 +402,7 @@ export class WebRTCPlugin extends AutoenableMixinProvider implements DeviceCreat const message = await new Promise<{ connectionManagementId: string, updateSessionId: string, - }>((resolve, reject) => { + } & ConnectOptions>((resolve, reject) => { const close = () => { const str = 'Connection closed while waiting for message'; reject(new Error(str)); @@ -416,6 +416,8 @@ export class WebRTCPlugin extends AutoenableMixinProvider implements DeviceCreat } }); + message.username = request.username; + const { connectionManagementId, updateSessionId } = message; if (connectionManagementId) { cleanup.promise.finally(async () => { @@ -457,7 +459,12 @@ export class WebRTCPlugin extends AutoenableMixinProvider implements DeviceCreat const cp = await client.clientPromise; cp.on('close', () => cleanup.resolve('socket client closed')); - process.send(message, cp); + // TODO: remove process.send hack + // 12/16/2022 + if (sdk.connect) + sdk.connect(cp, message); + else + process.send(message, cp); } catch (e) { console.error("error negotiating browser RTCC signaling", e); @@ -492,10 +499,10 @@ export async function fork() { if (port) { const socket = net.connect(port, '127.0.0.1'); cleanup.promise.finally(() => socket.destroy()); - + const dc = pc.createDataChannel('rpc'); dc.message.subscribe(message => socket.write(message)); - + const debouncer = new DataChannelDebouncer({ send: u8 => dc.send(Buffer.from(u8)), }, e => { diff --git a/sdk/package-lock.json b/sdk/package-lock.json index 5206bf52c..8f2862b1b 100644 --- a/sdk/package-lock.json +++ b/sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/sdk", - "version": "0.2.28", + "version": "0.2.33", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/sdk", - "version": "0.2.28", + "version": "0.2.33", "license": "ISC", "dependencies": { "@babel/preset-typescript": "^7.16.7", diff --git a/sdk/package.json b/sdk/package.json index 9c166d786..2796fd796 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/sdk", - "version": "0.2.28", + "version": "0.2.33", "description": "", "main": "dist/src/index.js", "exports": { diff --git a/sdk/src/acl.ts b/sdk/src/acl.ts new file mode 100644 index 000000000..e64e779ff --- /dev/null +++ b/sdk/src/acl.ts @@ -0,0 +1,3 @@ +export function mergeScryptedAccessControl() { + +} diff --git a/sdk/types/package-lock.json b/sdk/types/package-lock.json index fd5039c5e..6bb99d73d 100644 --- a/sdk/types/package-lock.json +++ b/sdk/types/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/types", - "version": "0.2.25", + "version": "0.2.30", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/types", - "version": "0.2.25", + "version": "0.2.30", "license": "ISC", "devDependencies": { "@types/rimraf": "^3.0.2", diff --git a/sdk/types/package.json b/sdk/types/package.json index c55ead186..e8a548603 100644 --- a/sdk/types/package.json +++ b/sdk/types/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/types", - "version": "0.2.25", + "version": "0.2.30", "description": "", "main": "dist/index.js", "author": "", diff --git a/sdk/types/scrypted_python/scrypted_sdk/types.py b/sdk/types/scrypted_python/scrypted_sdk/types.py index a37b745fa..fd5e3e373 100644 --- a/sdk/types/scrypted_python/scrypted_sdk/types.py +++ b/sdk/types/scrypted_python/scrypted_sdk/types.py @@ -73,7 +73,6 @@ class ScryptedInterface(Enum): AirQualitySensor = "AirQualitySensor" AmbientLightSensor = "AmbientLightSensor" AudioSensor = "AudioSensor" - Authenticator = "Authenticator" Battery = "Battery" BinarySensor = "BinarySensor" Brightness = "Brightness" @@ -130,9 +129,9 @@ class ScryptedInterface(Enum): Scriptable = "Scriptable" ScryptedDevice = "ScryptedDevice" ScryptedPlugin = "ScryptedPlugin" + ScryptedUser = "ScryptedUser" SecuritySystem = "SecuritySystem" Settings = "Settings" - SoftwareUpdate = "SoftwareUpdate" StartStop = "StartStop" TamperSensor = "TamperSensor" TemperatureSetting = "TemperatureSetting" @@ -235,6 +234,12 @@ class PictureDimensions(TypedDict): width: float pass +class ScryptedDeviceAccessControl(TypedDict): + id: str + methods: list[str] + properties: list[str] + pass + class VideoStreamOptions(TypedDict): bitrate: float bitrateControl: Any | Any @@ -559,6 +564,10 @@ class ScriptSource(TypedDict): script: str pass +class ScryptedUserAccessControl(TypedDict): + devicesAccessControls: list[ScryptedDeviceAccessControl] + pass + class SecuritySystemState(TypedDict): mode: SecuritySystemMode obstruction: SecuritySystemObstruction @@ -582,6 +591,18 @@ class Setting(TypedDict): value: SettingValue pass +class TemperatureCommand(TypedDict): + mode: ThermostatMode + setpoint: float | tuple[float, float] + pass + +class TemperatureSettingStatus(TypedDict): + activeMode: ThermostatMode + availableModes: list[ThermostatMode] + mode: ThermostatMode + setpoint: float | tuple[float, float] + pass + class VideoClip(TypedDict): description: str detectionClasses: list[str] @@ -616,11 +637,6 @@ class AudioSensor: audioDetected: bool pass -class Authenticator: - async def checkPassword(self, password: str) -> bool: - pass - pass - class Battery: batteryLevel: float pass @@ -697,7 +713,7 @@ class DeviceProvider: class Display: async def startDisplay(self, media: MediaObject) -> None: pass - async def stopDisplay(self, media: MediaObject) -> None: + async def stopDisplay(self) -> None: pass pass @@ -708,7 +724,7 @@ class Dock: pass class EngineIOHandler: - async def onConnection(self, request: HttpRequest, webSocketUrl: str) -> None: + async def onConnection(self, request: HttpRequest, webScoket: WebSocket) -> None: pass pass @@ -964,6 +980,11 @@ class ScryptedPlugin: pass pass +class ScryptedUser: + async def getScryptedUserAccessControl(self) -> ScryptedUserAccessControl: + pass + pass + class SecuritySystem: securitySystemState: SecuritySystemState async def armSecuritySystem(self, mode: SecuritySystemMode) -> None: @@ -979,14 +1000,6 @@ class Settings: pass pass -class SoftwareUpdate: - updateAvailable: bool - async def checkForUpdate(self) -> bool: - pass - async def installUpdate(self) -> None: - pass - pass - class StartStop: running: bool async def start(self) -> None: @@ -1000,12 +1013,15 @@ class TamperSensor: pass class TemperatureSetting: + temperatureSetting: TemperatureSettingStatus thermostatActiveMode: ThermostatMode thermostatAvailableModes: list[ThermostatMode] thermostatMode: ThermostatMode thermostatSetpoint: float thermostatSetpointHigh: float thermostatSetpointLow: float + async def setTemperature(self, command: TemperatureCommand) -> None: + pass async def setThermostatMode(self, mode: ThermostatMode) -> None: pass async def setThermostatSetpoint(self, degrees: float) -> None: @@ -1215,6 +1231,7 @@ class ScryptedInterfaceProperty(Enum): running = "running" paused = "paused" docked = "docked" + temperatureSetting = "temperatureSetting" thermostatActiveMode = "thermostatActiveMode" thermostatAvailableModes = "thermostatAvailableModes" thermostatMode = "thermostatMode" @@ -1229,7 +1246,6 @@ class ScryptedInterfaceProperty(Enum): entryOpen = "entryOpen" batteryLevel = "batteryLevel" online = "online" - updateAvailable = "updateAvailable" fromMimeType = "fromMimeType" toMimeType = "toMimeType" binaryState = "binaryState" @@ -1407,6 +1423,13 @@ class DeviceState: def docked(self, value: bool): self.setScryptedProperty("docked", value) + @property + def temperatureSetting(self) -> TemperatureSettingStatus: + return self.getScryptedProperty("temperatureSetting") + @temperatureSetting.setter + def temperatureSetting(self, value: TemperatureSettingStatus): + self.setScryptedProperty("temperatureSetting", value) + @property def thermostatActiveMode(self) -> ThermostatMode: return self.getScryptedProperty("thermostatActiveMode") @@ -1505,13 +1528,6 @@ class DeviceState: def online(self, value: bool): self.setScryptedProperty("online", value) - @property - def updateAvailable(self) -> bool: - return self.getScryptedProperty("updateAvailable") - @updateAvailable.setter - def updateAvailable(self, value: bool): - self.setScryptedProperty("updateAvailable", value) - @property def fromMimeType(self) -> str: return self.getScryptedProperty("fromMimeType") @@ -1794,12 +1810,14 @@ ScryptedInterfaceDescriptors = { "TemperatureSetting": { "name": "TemperatureSetting", "methods": [ + "setTemperature", "setThermostatMode", "setThermostatSetpoint", "setThermostatSetpointHigh", "setThermostatSetpointLow" ], "properties": [ + "temperatureSetting", "thermostatActiveMode", "thermostatAvailableModes", "thermostatMode", @@ -1926,13 +1944,6 @@ ScryptedInterfaceDescriptors = { ], "properties": [] }, - "Authenticator": { - "name": "Authenticator", - "methods": [ - "checkPassword" - ], - "properties": [] - }, "Scene": { "name": "Scene", "methods": [ @@ -2013,16 +2024,6 @@ ScryptedInterfaceDescriptors = { "online" ] }, - "SoftwareUpdate": { - "name": "SoftwareUpdate", - "methods": [ - "checkForUpdate", - "installUpdate" - ], - "properties": [ - "updateAvailable" - ] - }, "BufferConverter": { "name": "BufferConverter", "methods": [ @@ -2285,6 +2286,13 @@ ScryptedInterfaceDescriptors = { "properties": [ "applicationInfo" ] + }, + "ScryptedUser": { + "name": "ScryptedUser", + "methods": [ + "getScryptedUserAccessControl" + ], + "properties": [] } } diff --git a/sdk/types/src/build.ts b/sdk/types/src/build.ts index eef344ea9..bb8a93067 100644 --- a/sdk/types/src/build.ts +++ b/sdk/types/src/build.ts @@ -31,6 +31,7 @@ for (const name of Object.values(ScryptedInterface)) { } const properties = Object.values(ScryptedInterfaceDescriptors).map(d => d.properties).flat(); +const methods = Object.values(ScryptedInterfaceDescriptors).map(d => d.methods).flat(); const deviceStateContents = ` export interface DeviceState { @@ -48,11 +49,18 @@ ${properties.map(property => ' ' + property + ' = \"' + property + '",\n').join } `; +const methodContents = ` +export enum ScryptedInterfaceMethod { +${methods.map(method => ' ' + method + ' = \"' + method + '",\n').join('')} +} +`; + const contents = ` export const TYPES_VERSION = "${typesVersion}"; ${deviceStateContents} ${propertyContents} +${methodContents} export const ScryptedInterfaceDescriptors: { [scryptedInterface: string]: ScryptedInterfaceDescriptor } = ${stringifyObject(ScryptedInterfaceDescriptors, { indent: ' ' })} diff --git a/sdk/types/src/types.input.ts b/sdk/types/src/types.input.ts index 8430c6e6f..d34f7f766 100644 --- a/sdk/types/src/types.input.ts +++ b/sdk/types/src/types.input.ts @@ -1,4 +1,5 @@ import type { Worker as NodeWorker } from 'worker_threads'; +import type { Socket as NodeNetSocket } from 'net'; export type ScryptedNativeId = string | undefined; @@ -253,23 +254,77 @@ export interface Dock { docked?: boolean; } + +export interface TemperatureCommand { + mode?: ThermostatMode; + setpoint?: number | [number, number]; +} +export interface TemperatureSettingStatus { + availableModes?: ThermostatMode[]; + mode?: ThermostatMode; + activeMode?: ThermostatMode; + setpoint?: number | [number, number]; +} /** * TemperatureSetting represents a thermostat device. */ export interface TemperatureSetting { + temperatureSetting?: TemperatureSettingStatus; + setTemperature(command: TemperatureCommand): Promise; + + /** + * @deprecated + * @param mode + */ setThermostatMode(mode: ThermostatMode): Promise; + /** + * @deprecated + * @param mode + */ setThermostatSetpoint(degrees: number): Promise; + /** + * @deprecated + * @param mode + */ setThermostatSetpointHigh(high: number): Promise; + /** + * @deprecated + * @param mode + */ setThermostatSetpointLow(low: number): Promise; + /** + * @deprecated + * @param mode + */ thermostatAvailableModes?: ThermostatMode[]; + /** + * @deprecated + * @param mode + */ thermostatMode?: ThermostatMode; + /** + * @deprecated + * @param mode + */ thermostatActiveMode?: ThermostatMode; + /** + * @deprecated + * @param mode + */ thermostatSetpoint?: number; + /** + * @deprecated + * @param mode + */ thermostatSetpointHigh?: number; + /** + * @deprecated + * @param mode + */ thermostatSetpointLow?: number; } export enum HumidityMode { @@ -683,7 +738,7 @@ export interface PanTiltZoom { */ export interface Display { startDisplay(media: MediaObject): Promise; - stopDisplay(media: MediaObject): Promise; + stopDisplay(): Promise; } /** @@ -712,13 +767,7 @@ export interface PasswordStore { removePassword(password: string): Promise; } -/** - * Authenticator can be used to require a password before allowing interaction with a security device. - */ -export interface Authenticator { - checkPassword(password: string): Promise; -} /** * Scenes control multiple different devices into a given state. */ @@ -947,16 +996,6 @@ export interface Scriptable { loadScripts(): Promise<{ [filename: string]: ScriptSource }>; eval(source: ScriptSource, variables?: { [name: string]: any }): Promise; } -/** - * SoftwareUpdate provides a way to check for updates and install them. This may be a Scrypted Plugin or device firmware. - */ -export interface SoftwareUpdate { - checkForUpdate(): Promise; - - installUpdate(): Promise; - - updateAvailable?: boolean; -} export interface BufferConvertorOptions { sourceId?: string; @@ -1566,7 +1605,7 @@ export interface HttpResponseOptions { headers?: object; } export interface EngineIOHandler { - onConnection(request: HttpRequest, webSocketUrl: string): Promise; + onConnection(request: HttpRequest, webScoket: WebSocket): Promise; } /** @@ -1664,7 +1703,6 @@ export enum ScryptedInterface { Intercom = "Intercom", Lock = "Lock", PasswordStore = "PasswordStore", - Authenticator = "Authenticator", Scene = "Scene", Entry = "Entry", EntrySensor = "EntrySensor", @@ -1675,7 +1713,6 @@ export enum ScryptedInterface { Refresh = "Refresh", MediaPlayer = "MediaPlayer", Online = "Online", - SoftwareUpdate = "SoftwareUpdate", BufferConverter = "BufferConverter", Settings = "Settings", BinarySensor = "BinarySensor", @@ -1711,6 +1748,7 @@ export enum ScryptedInterface { RTCSignalingChannel = "RTCSignalingChannel", RTCSignalingClient = "RTCSignalingClient", LauncherApplication = "LauncherApplication", + ScryptedUser = "ScryptedUser", } /** @@ -1869,6 +1907,62 @@ export interface PluginFork { worker: NodeWorker; } +export declare interface DeviceState { + id?: string; + setState?(property: string, value: any): Promise; +} + +export interface ScryptedInterfaceDescriptor { + name: string; + properties: string[]; + methods: string[]; +} + +/** + * ScryptedDeviceAccessControl describes the methods and properties on a device + * that will be visible to the user. + * If methods is null, the user will be granted full access to all methods. + * If properties is null, the user will be granted full access to all properties. + */ +export interface ScryptedDeviceAccessControl { + id: string; + methods?: string[]; + properties?: string[]; +} + +/** + * ScryptedUserAccessControl describes the list of devices that + * may be accessed by the user. + */ +export interface ScryptedUserAccessControl { + /** + * If devicesAccessControls is null, the user has full access to all devices. + */ + devicesAccessControls: ScryptedDeviceAccessControl[] | null; +} + +/** + * ScryptedUser represents a user managed by Scrypted. + * This interface can not be implemented, only extended by Mixins. + */ +export interface ScryptedUser { + /** + * Retrieve the ScryptedUserAccessControl for a user. If no access control object is returned + * the user has full access to all devices. This differs from an admin user that can also + * access admin related system services. + */ + getScryptedUserAccessControl(): Promise; +} + +export interface APIOptions { + username?: string; + accessControls?: ScryptedUserAccessControl; +} + +export interface ConnectOptions extends APIOptions { + pluginId: string; +} + export interface ScryptedStatic { /** * @deprecated @@ -1883,16 +1977,15 @@ export interface ScryptedStatic { pluginHostAPI: any; pluginRemoteAPI: any; + /** + * Start a new instance of the plugin, returning an instance of the new process + * and the result of the fork method. + */ fork?(): PluginFork; -} - -export declare interface DeviceState { - id?: string; - setState?(property: string, value: any): Promise; -} - -export interface ScryptedInterfaceDescriptor { - name: string; - properties: string[]; - methods: string[]; + /** + * Initiate the Scrypted RPC wire protocol on a socket. + * @param socket + * @param options + */ + connect?(socket: NodeNetSocket, options?: ConnectOptions): void; } diff --git a/server/package-lock.json b/server/package-lock.json index 4000d0856..df8c2fdb1 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@ffmpeg-installer/ffmpeg": "^1.1.0", "@mapbox/node-pre-gyp": "^1.0.10", - "@scrypted/types": "^0.2.18", + "@scrypted/types": "^0.2.29", "adm-zip": "^0.5.9", "axios": "^0.21.4", "body-parser": "^1.19.0", @@ -245,9 +245,9 @@ } }, "node_modules/@scrypted/types": { - "version": "0.2.18", - "resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.18.tgz", - "integrity": "sha512-LEhdAgpWZbVMDt74zM/jqBQr42xQl4fDaGwAGtwz0XJ1xnx/hXBXVLs+SdP+Gtnoujjqd1kWTdZCDrnPP4/luw==" + "version": "0.2.29", + "resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.29.tgz", + "integrity": "sha512-l6BCe+2jHPnLaKbUfNxjtgfBkzzIVDhUo8iTjAKyJUfdWZsHZ8IFkzP9GTwenUHmOapDwYDboRum9NCxnjt7AA==" }, "node_modules/@tootallnate/once": { "version": "1.1.2", @@ -3281,9 +3281,9 @@ } }, "@scrypted/types": { - "version": "0.2.18", - "resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.18.tgz", - "integrity": "sha512-LEhdAgpWZbVMDt74zM/jqBQr42xQl4fDaGwAGtwz0XJ1xnx/hXBXVLs+SdP+Gtnoujjqd1kWTdZCDrnPP4/luw==" + "version": "0.2.29", + "resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.29.tgz", + "integrity": "sha512-l6BCe+2jHPnLaKbUfNxjtgfBkzzIVDhUo8iTjAKyJUfdWZsHZ8IFkzP9GTwenUHmOapDwYDboRum9NCxnjt7AA==" }, "@tootallnate/once": { "version": "1.1.2", diff --git a/server/package.json b/server/package.json index 7def48565..1305a409b 100644 --- a/server/package.json +++ b/server/package.json @@ -5,7 +5,7 @@ "dependencies": { "@ffmpeg-installer/ffmpeg": "^1.1.0", "@mapbox/node-pre-gyp": "^1.0.10", - "@scrypted/types": "^0.2.18", + "@scrypted/types": "^0.2.29", "adm-zip": "^0.5.9", "axios": "^0.21.4", "body-parser": "^1.19.0", diff --git a/server/src/db-types.ts b/server/src/db-types.ts index 20d9472c6..a86a0fb97 100644 --- a/server/src/db-types.ts +++ b/server/src/db-types.ts @@ -28,7 +28,7 @@ export class ScryptedAlert extends ScryptedDocument { title: string; path: string; message: string; -}`` +} export class PluginDevice extends ScryptedDocument { constructor(id?: string) { diff --git a/server/src/plugin/plugin-api.ts b/server/src/plugin/plugin-api.ts index 380faf05a..fa088d529 100644 --- a/server/src/plugin/plugin-api.ts +++ b/server/src/plugin/plugin-api.ts @@ -22,9 +22,6 @@ export interface PluginAPI { listen(EventListener: (id: string, eventDetails: EventDetails, eventData: any) => void): Promise; listenDevice(id: string, event: string | EventListenerOptions, callback: (eventDetails: EventDetails, eventData: any) => void): Promise; - ioClose(id: string): Promise; - ioSend(id: string, message: string): Promise; - deliverPush(endpoint: string, request: HttpRequest): Promise; getLogger(nativeId: ScryptedNativeId): Promise; @@ -113,12 +110,6 @@ export class PluginAPIProxy extends PluginAPIManagedListeners implements PluginA async listenDevice(id: string, event: string | EventListenerOptions, callback: (eventDetails: EventDetails, eventData: any) => void): Promise { return this.manageListener(await this.api.listenDevice(id, event, callback)); } - ioClose(id: string): Promise { - return this.api.ioClose(id); - } - ioSend(id: string, message: string): Promise { - return this.api.ioSend(id, message); - } deliverPush(endpoint: string, request: HttpRequest): Promise { return this.api.deliverPush(endpoint, request); } diff --git a/server/src/plugin/plugin-host-api.ts b/server/src/plugin/plugin-host-api.ts index 19fa2ff73..507f74042 100644 --- a/server/src/plugin/plugin-host-api.ts +++ b/server/src/plugin/plugin-host-api.ts @@ -20,8 +20,6 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP 'onMixinEvent', 'onDeviceEvent', 'setStorage', - 'ioSend', - 'ioClose', 'setDeviceProperty', 'deliverPush', 'requestRestart', @@ -105,18 +103,6 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP } } - async ioClose(id: string) { - // @ts-expect-error - this.pluginHost.io.clients[id]?.close(); - this.pluginHost.ws[id]?.close(); - } - - async ioSend(id: string, message: string) { - // @ts-expect-error - this.pluginHost.io.clients[id]?.send(message); - this.pluginHost.ws[id]?.send(message); - } - async setState(nativeId: ScryptedNativeId, key: string, value: any) { checkProperty(key, value); this.scrypted.stateManager.setPluginState(this.pluginId, nativeId, this.propertyInterfaces?.[key], key, value); diff --git a/server/src/plugin/plugin-host.ts b/server/src/plugin/plugin-host.ts index a0f4d6f49..ef76f5020 100644 --- a/server/src/plugin/plugin-host.ts +++ b/server/src/plugin/plugin-host.ts @@ -168,7 +168,14 @@ export class PluginHost { }); // @ts-expect-error - await handler.onConnection(endpointRequest, new WebSocketConnection(`io://${id}`)); + await handler.onConnection(endpointRequest, new WebSocketConnection(`io://${id}`, { + send(message) { + socket.send(message); + }, + close(message) { + socket.close(); + }, + })); } catch (e) { console.error('engine.io plugin error', e); @@ -304,7 +311,6 @@ export class PluginHost { }); this.worker.setupRpcPeer(this.peer); - this.peer.addSerializer(WebSocketConnection, WebSocketConnection.name, new WebSocketSerializer()); this.worker.stdout.on('data', data => console.log(data.toString())); this.worker.stderr.on('data', data => console.error(data.toString())); @@ -373,7 +379,6 @@ export class PluginHost { } }); serializer.setupRpcPeer(rpcPeer); - rpcPeer.addSerializer(WebSocketConnection, WebSocketConnection.name, new WebSocketSerializer()); // wrap the host api with a connection specific api that can be torn down on disconnect const createMediaManager = await this.peer.getParam('createMediaManager'); @@ -390,7 +395,6 @@ export class PluginHost { async createRpcPeer(duplex: Duplex) { const rpcPeer = createDuplexRpcPeer(`api/${this.pluginId}`, 'duplex', duplex, duplex); - rpcPeer.addSerializer(WebSocketConnection, WebSocketConnection.name, new WebSocketSerializer()); // wrap the host api with a connection specific api that can be torn down on disconnect const createMediaManager = await this.peer.getParam('createMediaManager'); diff --git a/server/src/plugin/plugin-remote-websocket.ts b/server/src/plugin/plugin-remote-websocket.ts index 8465d1907..2faf9a533 100644 --- a/server/src/plugin/plugin-remote-websocket.ts +++ b/server/src/plugin/plugin-remote-websocket.ts @@ -62,7 +62,7 @@ export interface WebSocketConnectCallbacks { } export interface WebSocketConnect { - (url: string, callbacks: WebSocketConnectCallbacks): void; + (connection: WebSocketConnection, callbacks: WebSocketConnectCallbacks): void; } export interface WebSocketMethods { @@ -76,15 +76,14 @@ export function createWebSocketClass(__websocketConnect: WebSocketConnect): any _url: string; _protocols: string[]; readyState: number; - _ws: WebSocketMethods; - constructor(url: string, protocols?: string[]) { + constructor(public connection: WebSocketConnection, protocols?: string[]) { super(); - this._url = url; + this._url = connection.url; this._protocols = protocols; this.readyState = 0; - __websocketConnect(url, { + __websocketConnect(connection, { connect: (e, ws) => { // connect if (e != null) { @@ -95,7 +94,6 @@ export function createWebSocketClass(__websocketConnect: WebSocketConnect): any return; } - this._ws = ws; this.readyState = 1; this.dispatchEvent({ type: 'open', @@ -129,7 +127,7 @@ export function createWebSocketClass(__websocketConnect: WebSocketConnect): any } send(message: string | ArrayBufferLike) { - this._ws.send(message); + this.connection.send(message); } get url() { @@ -141,7 +139,7 @@ export function createWebSocketClass(__websocketConnect: WebSocketConnect): any } close(reason: string) { - this._ws.close(reason); + this.connection.close(reason); } } @@ -153,10 +151,25 @@ export function createWebSocketClass(__websocketConnect: WebSocketConnect): any return WebSocket; } -export class WebSocketConnection { +export class WebSocketConnection implements WebSocketMethods { [RpcPeer.PROPERTY_PROXY_PROPERTIES]: any; - constructor(public url: string) { + [RpcPeer.PROPERTY_PROXY_ONEWAY_METHODS] = [ + "send", + "close", + ]; + + constructor(public url: string, public websocketMethods: WebSocketMethods) { + this[RpcPeer.PROPERTY_PROXY_PROPERTIES] = { + url, + } + } + + send(message: string | ArrayBufferLike): void { + return this.websocketMethods.send(message); + } + close(message: string): void { + return this.websocketMethods.close(message); } } @@ -164,16 +177,12 @@ export class WebSocketSerializer implements RpcSerializer { WebSocket: ReturnType; serialize(value: any, serializationContext?: any) { - const connection = value as WebSocketConnection; - connection[RpcPeer.PROPERTY_PROXY_PROPERTIES] = { - url: connection.url, - } - return connection; + throw new Error("WebSocketSerializer should only be used for deserialization."); } - deserialize(serialized: any, serializationContext?: any) { + deserialize(serialized: WebSocketConnection, serializationContext?: any) { if (!this.WebSocket) return undefined; - return new this.WebSocket(serialized.url); + return new this.WebSocket(serialized); } } diff --git a/server/src/plugin/plugin-remote-worker.ts b/server/src/plugin/plugin-remote-worker.ts index c3629d80f..a32ba3a2f 100644 --- a/server/src/plugin/plugin-remote-worker.ts +++ b/server/src/plugin/plugin-remote-worker.ts @@ -336,6 +336,9 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa pluginReader = undefined; const script = main.toString(); + scrypted.connect = (socket, options) => { + process.send(options, socket); + } const forks = new Set(); diff --git a/server/src/plugin/plugin-remote.ts b/server/src/plugin/plugin-remote.ts index 4609ea433..b08634305 100644 --- a/server/src/plugin/plugin-remote.ts +++ b/server/src/plugin/plugin-remote.ts @@ -429,15 +429,16 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp const retPromise = new Promise(resolve => done = resolve); peer.params.getRemote = async (api: PluginAPI, pluginId: string) => { - websocketSerializer.WebSocket = createWebSocketClass((url: string, callbacks: WebSocketConnectCallbacks) => { + websocketSerializer.WebSocket = createWebSocketClass((connection, callbacks) => { + const {url} = connection; if (url.startsWith('io://') || url.startsWith('ws://')) { const id = url.substring('xx://'.length); ioSockets[id] = callbacks; callbacks.connect(undefined, { - close: () => api.ioClose(id), - send: (message: string) => api.ioSend(id, message), + close: (message) => connection.close(message), + send: (message) => connection.send(message), }); } else { diff --git a/server/src/rpc.ts b/server/src/rpc.ts index 89815c754..75cb3dc64 100644 --- a/server/src/rpc.ts +++ b/server/src/rpc.ts @@ -393,6 +393,12 @@ export class RpcPeer { if (!proxy) proxy = this.newProxy(__remote_proxy_id, __remote_constructor_name, __remote_proxy_props, __remote_proxy_oneway_methods); proxy[RpcPeer.finalizerIdSymbol] = __remote_proxy_finalizer_id; + + const deserializer = this.nameDeserializerMap.get(__remote_constructor_name); + if (deserializer) { + return deserializer.deserialize(proxy, deserializationContext); + } + return proxy; } diff --git a/server/src/runtime.ts b/server/src/runtime.ts index 867f60c86..9d545e25e 100644 --- a/server/src/runtime.ts +++ b/server/src/runtime.ts @@ -345,7 +345,14 @@ export class ScryptedRuntime extends PluginHttp { }); // @ts-expect-error - await handler.onConnection(httpRequest, new WebSocketConnection(`ws://${id}`)); + await handler.onConnection(httpRequest, new WebSocketConnection(`ws://${id}`, { + send(message) { + ws.send(message); + }, + close(message) { + ws.close(); + }, + })); } async getComponent(componentId: string): Promise {