mirror of
https://github.com/koush/scrypted.git
synced 2026-02-06 23:42:19 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43e5822c93 | ||
|
|
bc579514e7 | ||
|
|
825100f94e | ||
|
|
803bfc1560 | ||
|
|
b2013a54ed | ||
|
|
f252407935 | ||
|
|
516f2a2a7b | ||
|
|
c1677ce691 | ||
|
|
5028fb812d | ||
|
|
2db4e2579f | ||
|
|
b339ca6cd2 | ||
|
|
f100999cb1 | ||
|
|
2863756bd6 | ||
|
|
cc408850a0 | ||
|
|
ed1ceeda51 | ||
|
|
df09d8e92a | ||
|
|
298ac960d1 | ||
|
|
62d4d55aae | ||
|
|
a2121c0dc5 | ||
|
|
9b5ea27c0b | ||
|
|
0b0e90fc04 | ||
|
|
d8aff609bf | ||
|
|
d8283c261a | ||
|
|
e3aca964be | ||
|
|
f97669949d |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.2.3",
|
||||
"version": "0.2.4",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
|
||||
@@ -15,6 +15,11 @@ const includeToken = 4;
|
||||
|
||||
export let DEBUG = false;
|
||||
|
||||
function debug(...args: any[]) {
|
||||
if (DEBUG)
|
||||
console.debug(...args);
|
||||
}
|
||||
|
||||
class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, MixinProvider, Settings {
|
||||
storageSettings = new StorageSettings(this, {
|
||||
tokenInfo: {
|
||||
@@ -34,6 +39,14 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
description: 'This is the endpoint Alexa will use to send events to. This is set after you login.',
|
||||
type: 'string',
|
||||
readonly: true
|
||||
},
|
||||
debug: {
|
||||
title: 'Debug Events',
|
||||
description: 'Log all events to the console. This will be very noisy and should not be left enabled.',
|
||||
type: 'boolean',
|
||||
onPut(oldValue: boolean, newValue: boolean) {
|
||||
DEBUG = newValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -44,6 +57,8 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
constructor(nativeId?: string) {
|
||||
super(nativeId);
|
||||
|
||||
DEBUG = this.storageSettings.values.debug ?? false;
|
||||
|
||||
alexaHandlers.set('Alexa.Authorization/AcceptGrant', this.onAlexaAuthorization);
|
||||
alexaHandlers.set('Alexa.Discovery/Discover', this.onDiscoverEndpoints);
|
||||
|
||||
@@ -141,12 +156,23 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
if (!supportedType)
|
||||
return;
|
||||
|
||||
const report = await supportedType.sendEvent(eventSource, eventDetails, eventData);
|
||||
let report = await supportedType.sendEvent(eventSource, eventDetails, eventData);
|
||||
|
||||
if (!report && eventDetails.eventInterface === ScryptedInterface.Online) {
|
||||
report = {};
|
||||
}
|
||||
|
||||
if (!report && eventDetails.eventInterface === ScryptedInterface.Battery) {
|
||||
report = {};
|
||||
}
|
||||
|
||||
if (!report) {
|
||||
this.console.warn(`${eventDetails.eventInterface}.${eventDetails.property} not supported for device ${eventSource.type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
debug("event", eventDetails.eventInterface, eventDetails.property, eventSource.type);
|
||||
|
||||
let data = {
|
||||
"event": {
|
||||
"header": {
|
||||
@@ -234,7 +260,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
const endpoint = await this.getAlexaEndpoint();
|
||||
const self = this;
|
||||
|
||||
this.console.assert(!DEBUG, `event:`, data);
|
||||
debug("send event to alexa", data);
|
||||
|
||||
return axios.post(`https://${endpoint}/v3/events`, data, {
|
||||
headers: {
|
||||
@@ -570,6 +596,8 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
const { authorization } = request.headers;
|
||||
if (!this.validAuths.has(authorization)) {
|
||||
try {
|
||||
debug("making authorization request to Scrypted");
|
||||
|
||||
await axios.get('https://home.scrypted.app/_punch/getcookie', {
|
||||
headers: {
|
||||
'Authorization': authorization,
|
||||
@@ -590,11 +618,11 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
const { directive } = body;
|
||||
const { namespace, name } = directive.header;
|
||||
|
||||
this.console.assert(!DEBUG, `request: ${namespace}/${name}`);
|
||||
|
||||
const mapName = `${namespace}/${name}`;
|
||||
const handler = alexaHandlers.get(mapName);
|
||||
|
||||
debug("received directive from alexa", mapName, body);
|
||||
|
||||
const handler = alexaHandlers.get(mapName);
|
||||
if (handler)
|
||||
return handler.apply(this, [request, response, directive]);
|
||||
|
||||
@@ -641,7 +669,7 @@ class HttpResponseLoggingImpl implements AlexaHttpResponse {
|
||||
if (options.code !== 200)
|
||||
this.console.error(`response error ${options.code}:`, body);
|
||||
else
|
||||
this.console.assert(!DEBUG, `response ${options.code}:`, body);
|
||||
debug("response to alexa directive", options.code, body);
|
||||
|
||||
if (typeof body === 'object')
|
||||
body = JSON.stringify(body);
|
||||
|
||||
4
plugins/core/package-lock.json
generated
4
plugins/core/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.104",
|
||||
"version": "0.1.108",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.104",
|
||||
"version": "0.1.108",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.104",
|
||||
"version": "0.1.108",
|
||||
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -23,6 +23,15 @@ export class User extends ScryptedDeviceBase implements Settings, ScryptedUser {
|
||||
})
|
||||
|
||||
async getScryptedUserAccessControl(): Promise<ScryptedUserAccessControl> {
|
||||
const usersService = await sdk.systemManager.getComponent('users');
|
||||
const users: DBUser[] = await usersService.getAllUsers();
|
||||
const user = users.find(user => user.username === this.username);
|
||||
if (!user)
|
||||
throw new Error("user not found");
|
||||
|
||||
if (user.admin)
|
||||
return;
|
||||
|
||||
const self = sdk.deviceManager.getDeviceState(this.nativeId);
|
||||
|
||||
const ret: ScryptedUserAccessControl = {
|
||||
|
||||
@@ -215,6 +215,7 @@ import Notifier from "../interfaces/Notifier.vue";
|
||||
import OnOff from "../interfaces/OnOff.vue";
|
||||
import Brightness from "../interfaces/Brightness.vue";
|
||||
import Battery from "../interfaces/Battery.vue";
|
||||
import Charger from "../interfaces/Charger.vue";
|
||||
import Lock from "../interfaces/Lock.vue";
|
||||
import ColorSettingHsv from "../interfaces/ColorSettingHsv.vue";
|
||||
import ColorSettingRgb from "../interfaces/ColorSettingRgb.vue";
|
||||
@@ -263,6 +264,7 @@ const cardHeaderInterfaces = [
|
||||
ScryptedInterface.AudioSensor,
|
||||
ScryptedInterface.HumiditySensor,
|
||||
ScryptedInterface.Thermometer,
|
||||
ScryptedInterface.Charger,
|
||||
ScryptedInterface.Battery,
|
||||
ScryptedInterface.Lock,
|
||||
ScryptedInterface.OnOff,
|
||||
@@ -362,6 +364,7 @@ export default {
|
||||
|
||||
Lock,
|
||||
OnOff,
|
||||
Charger,
|
||||
Battery,
|
||||
Thermometer,
|
||||
HumiditySensor,
|
||||
|
||||
52
plugins/core/ui/src/interfaces/Charger.vue
Normal file
52
plugins/core/ui/src/interfaces/Charger.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<v-tooltip left>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon
|
||||
v-on="on"
|
||||
v-if="lazyValue.chargeState === Charging"
|
||||
class="mr-1 mr-1"
|
||||
small
|
||||
>fa-plug</v-icon>
|
||||
<v-icon
|
||||
v-on="on"
|
||||
v-else-if="lazyValue.chargeState == Trickle"
|
||||
class="mr-1 mr-1"
|
||||
small
|
||||
>fa-plug-circle-minus</v-icon>
|
||||
<v-icon
|
||||
v-on="on"
|
||||
v-else
|
||||
class="mr-1 mr-1"
|
||||
small
|
||||
>fa-plug-circle-xmark</v-icon>
|
||||
</template>
|
||||
<span>{{ chargeText }}</span>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ChargeState } from '@scrypted/types';
|
||||
import RPCInterface from "./RPCInterface.vue";
|
||||
|
||||
export default {
|
||||
mixins: [RPCInterface],
|
||||
data() {
|
||||
return {
|
||||
Charging: ChargeState.Charging,
|
||||
Trickle: ChargeState.Trickle,
|
||||
NotCharging: ChargeState.NotCharging,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
chargeText() {
|
||||
if (this.lazyValue.chargeState === "trickle") {
|
||||
return "Trickle Charging";
|
||||
}
|
||||
if (this.lazyValue.chargeState === "charging") {
|
||||
return "Charging";
|
||||
}
|
||||
return "Not Charging";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -41,8 +41,7 @@
|
||||
</template>
|
||||
</DevicePicker>
|
||||
<DevicePicker v-else-if="lazyValue.type === 'interface'" v-model="lazyValue.value" :multiple="lazyValue.multiple"
|
||||
:readonly="lazyValue.readonly" :devices="interfaces" :title="lazyValue.title"
|
||||
:description="lazyValue.description">
|
||||
:readonly="lazyValue.readonly" :devices="interfaces" :title="lazyValue.title" :description="lazyValue.description">
|
||||
<template v-slot:append-outer>
|
||||
<v-btn v-if="dirty && device" color="success" @click="save" class="shift-up">
|
||||
<v-icon>send</v-icon>
|
||||
@@ -52,7 +51,7 @@
|
||||
<div v-else-if="lazyValue.type === 'clippath'" class="mb-2">
|
||||
<v-btn small block @click="editZone">{{ lazyValue.title }} </v-btn>
|
||||
<Camera :value="device" :device="device" :clipPathValue="sanitizedClipPathValue" :showDialog="editingZone"
|
||||
:hidePreview="true" @dialog="editingZoneChanged" @clipPath="lazyValue.value = $event"></Camera>
|
||||
:hidePreview="true" @dialog="editingZoneChanged" @clipPath="updateClipPath"></Camera>
|
||||
</div>
|
||||
<v-textarea v-else-if="lazyValue.type === 'textarea'" v-model="lazyValue.value" outlined persistent-hint
|
||||
:hint="lazyValue.description" :label="lazyValue.title">
|
||||
@@ -88,6 +87,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
editingZone: false,
|
||||
clipPathThrottle: null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@@ -142,7 +142,7 @@ export default {
|
||||
);
|
||||
},
|
||||
set(val) {
|
||||
this.lazyValue.value = val.toString();
|
||||
this.lazyValue.value = !!val;
|
||||
},
|
||||
},
|
||||
dirty() {
|
||||
@@ -228,6 +228,17 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
onChange() { },
|
||||
updateClipPath(e) {
|
||||
clearTimeout(this.clipPathThrottle);
|
||||
this.clipPathThrottle = setTimeout(() => {
|
||||
this.lazyValue.value = e;
|
||||
this.rpc().putSetting(
|
||||
this.lazyValue.key,
|
||||
this.createInputValue().value
|
||||
);
|
||||
this.onInput();
|
||||
}, 500)
|
||||
},
|
||||
editingZoneChanged(value) {
|
||||
this.editingZone = value;
|
||||
if (!value) {
|
||||
|
||||
@@ -58,6 +58,8 @@ import {
|
||||
faLightbulb,
|
||||
faToggleOn,
|
||||
faPlug,
|
||||
faPlugCircleMinus,
|
||||
faPlugCircleXmark,
|
||||
faExclamationTriangle,
|
||||
faSun,
|
||||
faCode,
|
||||
@@ -150,6 +152,8 @@ const icons: IconDefinition[] =[
|
||||
faLightbulb,
|
||||
faToggleOn,
|
||||
faPlug,
|
||||
faPlugCircleMinus,
|
||||
faPlugCircleXmark,
|
||||
faExclamationTriangle,
|
||||
faSun,
|
||||
faCode,
|
||||
|
||||
4
plugins/homekit/package-lock.json
generated
4
plugins/homekit/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/homekit",
|
||||
"version": "1.2.22",
|
||||
"version": "1.2.23",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/homekit",
|
||||
"version": "1.2.22",
|
||||
"version": "1.2.23",
|
||||
"dependencies": {
|
||||
"@koush/werift-src": "file:../../external/werift",
|
||||
"check-disk-space": "^3.3.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/homekit",
|
||||
"version": "1.2.22",
|
||||
"version": "1.2.23",
|
||||
"description": "HomeKit Plugin for Scrypted",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
|
||||
4
plugins/objectdetector/package-lock.json
generated
4
plugins/objectdetector/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.0.121",
|
||||
"version": "0.0.122",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.0.121",
|
||||
"version": "0.0.122",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.0.121",
|
||||
"version": "0.0.122",
|
||||
"description": "Scrypted Video Analysis Plugin. Installed alongside a detection service like OpenCV or TensorFlow.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -61,6 +61,10 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
choices,
|
||||
}
|
||||
},
|
||||
onPut: () => {
|
||||
this.endObjectDetection();
|
||||
this.maybeStartMotionDetection();
|
||||
},
|
||||
defaultValue: 'Default',
|
||||
},
|
||||
motionSensorSupplementation: {
|
||||
|
||||
4
plugins/pam-diff/package-lock.json
generated
4
plugins/pam-diff/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/pam-diff",
|
||||
"version": "0.0.18",
|
||||
"version": "0.0.20",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/pam-diff",
|
||||
"version": "0.0.18",
|
||||
"version": "0.0.20",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@types/node": "^16.6.1",
|
||||
|
||||
@@ -43,5 +43,5 @@
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.0.18"
|
||||
"version": "0.0.20"
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { PassThrough, Writable } from 'stream';
|
||||
const { mediaManager } = sdk;
|
||||
|
||||
const defaultDifference = 9;
|
||||
const defaultPercentage = 15;
|
||||
const defaultPercentage = 2;
|
||||
|
||||
interface PamDiffSession {
|
||||
id: string;
|
||||
|
||||
4
plugins/prebuffer-mixin/package-lock.json
generated
4
plugins/prebuffer-mixin/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.9.79",
|
||||
"version": "0.9.80",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.9.79",
|
||||
"version": "0.9.80",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.9.79",
|
||||
"version": "0.9.80",
|
||||
"description": "Video Stream Rebroadcast, Prebuffer, and Management Plugin for Scrypted.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -111,7 +111,7 @@ export function createStreamSettings(device: MixinDeviceBase<VideoCamera>) {
|
||||
placeholder: '-hwaccel auto',
|
||||
choices: Object.keys(getH264DecoderArgs()),
|
||||
combobox: true,
|
||||
mapPut: (oldValue, newValue) => getH264DecoderArgs()[newValue]?.join(' ') || newValue,
|
||||
mapPut: (oldValue, newValue) => getH264DecoderArgs()[newValue]?.join(' ') || newValue || '',
|
||||
hide: true,
|
||||
},
|
||||
videoFilterArguments: {
|
||||
|
||||
4
plugins/python-codecs/package-lock.json
generated
4
plugins/python-codecs/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/python-codecs",
|
||||
"version": "0.1.27",
|
||||
"version": "0.1.30",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/python-codecs",
|
||||
"version": "0.1.27",
|
||||
"version": "0.1.30",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/python-codecs",
|
||||
"version": "0.1.27",
|
||||
"version": "0.1.30",
|
||||
"description": "Python Codecs for Scrypted",
|
||||
"keywords": [
|
||||
"scrypted",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import traceback
|
||||
import asyncio
|
||||
import scrypted_sdk
|
||||
from scrypted_sdk import Setting, SettingValue
|
||||
@@ -6,6 +7,7 @@ import gstreamer
|
||||
import libav
|
||||
import vipsimage
|
||||
import pilimage
|
||||
import time
|
||||
|
||||
Gst = None
|
||||
try:
|
||||
@@ -128,19 +130,29 @@ def create_scrypted_plugin():
|
||||
|
||||
class CodecFork:
|
||||
async def generateVideoFramesGstreamer(self, mediaObject: scrypted_sdk.MediaObject, options: scrypted_sdk.VideoFrameGeneratorOptions = None, filter: Any = None, h264Decoder: str = None) -> scrypted_sdk.VideoFrame:
|
||||
start = time.time()
|
||||
try:
|
||||
async for data in gstreamer.generateVideoFramesGstreamer(mediaObject, options, filter, h264Decoder):
|
||||
yield data
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
raise
|
||||
finally:
|
||||
print('gstreamer finished after %s' % (time.time() - start))
|
||||
import os
|
||||
os._exit(os.EX_OK)
|
||||
pass
|
||||
|
||||
async def generateVideoFramesLibav(self, mediaObject: scrypted_sdk.MediaObject, options: scrypted_sdk.VideoFrameGeneratorOptions = None, filter: Any = None) -> scrypted_sdk.VideoFrame:
|
||||
start = time.time()
|
||||
try:
|
||||
async for data in libav.generateVideoFramesLibav(mediaObject, options, filter):
|
||||
yield data
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
raise
|
||||
finally:
|
||||
print('libav finished after %s' % (time.time() - start))
|
||||
import os
|
||||
os._exit(os.EX_OK)
|
||||
pass
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# needed by libav to_ndarray
|
||||
numpy>=1.16.2
|
||||
|
||||
# gobject instrospection for gstreamer.
|
||||
PyGObject>=3.30.4; sys_platform != 'win32'
|
||||
|
||||
|
||||
6
plugins/unifi-protect/package-lock.json
generated
6
plugins/unifi-protect/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/unifi-protect",
|
||||
"version": "0.0.131",
|
||||
"version": "0.0.132",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/unifi-protect",
|
||||
"version": "0.0.131",
|
||||
"version": "0.0.132",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@koush/unifi-protect": "file:../../external/unifi-protect",
|
||||
@@ -61,7 +61,7 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.68",
|
||||
"version": "0.2.86",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/unifi-protect",
|
||||
"version": "0.0.131",
|
||||
"version": "0.0.132",
|
||||
"description": "Unifi Protect Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ffmpegLogInitialOutput, safeKillFFmpeg } from '@scrypted/common/src/media-helpers';
|
||||
import { fitHeightToWidth } from "@scrypted/common/src/resolution-utils";
|
||||
import sdk, { Camera, DeviceProvider, FFmpegInput, Intercom, MediaObject, MediaStreamOptions, MediaStreamUrl, MotionSensor, Notifier, NotifierOptions, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, PictureOptions, ResponseMediaStreamOptions, ResponsePictureOptions, ScryptedDeviceBase, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, VideoCamera, VideoCameraConfiguration } from "@scrypted/sdk";
|
||||
import sdk, { Camera, DeviceProvider, FFmpegInput, Intercom, MediaObject, MediaStreamOptions, MediaStreamUrl, MotionSensor, Notifier, NotifierOptions, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, Online, PanTiltZoom, PanTiltZoomCommand, PictureOptions, ResponseMediaStreamOptions, ResponsePictureOptions, ScryptedDeviceBase, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, VideoCamera, VideoCameraConfiguration } from "@scrypted/sdk";
|
||||
import child_process, { ChildProcess } from 'child_process';
|
||||
import { once } from "events";
|
||||
import { Readable } from "stream";
|
||||
@@ -38,7 +38,7 @@ export class UnifiPackageCamera extends ScryptedDeviceBase implements Camera, Vi
|
||||
}
|
||||
}
|
||||
|
||||
export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Intercom, Camera, VideoCamera, VideoCameraConfiguration, MotionSensor, Settings, ObjectDetector, DeviceProvider, OnOff {
|
||||
export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Intercom, Camera, VideoCamera, VideoCameraConfiguration, MotionSensor, Settings, ObjectDetector, DeviceProvider, OnOff, PanTiltZoom, Online {
|
||||
motionTimeout: NodeJS.Timeout;
|
||||
detectionTimeout: NodeJS.Timeout;
|
||||
ringTimeout: NodeJS.Timeout;
|
||||
@@ -61,6 +61,15 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
this.updateState(protectCamera);
|
||||
}
|
||||
|
||||
async ptzCommand(command: PanTiltZoomCommand): Promise<void> {
|
||||
const camera = this.findCamera() as any;
|
||||
await this.protect.api.updateCamera(camera, {
|
||||
ispSettings: {
|
||||
zoomPosition: Math.abs(command.zoom * 100),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async setStatusLight(on: boolean) {
|
||||
const camera = this.findCamera() as any;
|
||||
await this.protect.api.updateCamera(camera, {
|
||||
@@ -411,6 +420,11 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
if (!camera)
|
||||
return;
|
||||
this.on = !!camera.ledSettings?.isEnabled;
|
||||
this.online = !!camera.isConnected;
|
||||
this.setMotionDetected(!!camera.isMotionDetected);
|
||||
|
||||
if (!!camera.featureFlags.canOpticalZoom) {
|
||||
this.ptzCapabilities = { pan: false, tilt: false, zoom: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,6 +376,9 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
if (camera.featureFlags.hasLedStatus) {
|
||||
d.interfaces.push(ScryptedInterface.OnOff);
|
||||
}
|
||||
if (camera.featureFlags.canOpticalZoom) {
|
||||
d.interfaces.push(ScryptedInterface.PanTiltZoom);
|
||||
}
|
||||
d.interfaces.push(ScryptedInterface.ObjectDetector);
|
||||
devices.push(d);
|
||||
}
|
||||
|
||||
2
plugins/webrtc/.vscode/settings.json
vendored
2
plugins/webrtc/.vscode/settings.json
vendored
@@ -1,4 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
"scrypted.debugHost": "koushik-ubuntu",
|
||||
}
|
||||
4
plugins/webrtc/package-lock.json
generated
4
plugins/webrtc/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/webrtc",
|
||||
"version": "0.1.39",
|
||||
"version": "0.1.41",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/webrtc",
|
||||
"version": "0.1.39",
|
||||
"version": "0.1.41",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/webrtc",
|
||||
"version": "0.1.39",
|
||||
"version": "0.1.41",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
|
||||
@@ -128,6 +128,7 @@ class WebRTCMixin extends SettingsMixinDeviceBase<RTCSignalingClient & VideoCame
|
||||
this.plugin.storageSettings.values.maximumCompatibilityMode,
|
||||
this.plugin.getRTCConfiguration(),
|
||||
await this.plugin.getWeriftConfiguration(options?.disableTurn),
|
||||
options?.requiresAnswer === true ? false : true,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { RtpPacket } from "../../../external/werift/packages/rtp/src/rtp/rtp";
|
||||
import { Deferred } from "@scrypted/common/src/deferred";
|
||||
import { closeQuiet, createBindZero, listenZeroSingleClient, reserveUdpPort } from "@scrypted/common/src/listen-cluster";
|
||||
import { ffmpegLogInitialOutput, safeKillFFmpeg, safePrintFFmpegArguments } from "@scrypted/common/src/media-helpers";
|
||||
@@ -232,8 +233,6 @@ export async function startRtpForwarderProcess(console: Console, ffmpegInput: FF
|
||||
// if the rtsp client is over tcp, then the restream server must also be tcp, as
|
||||
// the rtp packets (which can be a max of 64k) may be too large for udp.
|
||||
const clientIsTcp = await setupRtspClient(console, rtspClient, channel, audioSection, false, rtp => {
|
||||
const payload = rtp.subarray(12);
|
||||
|
||||
// live555 sends rtp aac packets without AU header followed by ADTS packets (which contain codec info)
|
||||
// which ffmpeg can not handle.
|
||||
// the solution is to demux the adts and send that to ffmpeg raw.
|
||||
@@ -241,8 +240,10 @@ export async function startRtpForwarderProcess(console: Console, ffmpegInput: FF
|
||||
if (firstPacket) {
|
||||
firstPacket = false;
|
||||
if (audioSection.codec === 'aac') {
|
||||
const packet = RtpPacket.deSerialize(rtp);
|
||||
const buf = packet.payload;
|
||||
// adts header is 12 bits of 1s
|
||||
if (payload[0] == 0xff && (payload[1] & 0xf0) == 0xf0) {
|
||||
if (buf[0] == 0xff && (buf[1] & 0xf0) == 0xf0) {
|
||||
adts = true;
|
||||
allowAudioTranscoderExit = true;
|
||||
const ffmpegArgs = [
|
||||
@@ -270,7 +271,8 @@ export async function startRtpForwarderProcess(console: Console, ffmpegInput: FF
|
||||
rtspServer?.sendTrack(audioControl, rtp, false);
|
||||
}
|
||||
else {
|
||||
audioPipe?.write(payload);
|
||||
const packet = RtpPacket.deSerialize(rtp);
|
||||
audioPipe?.write(packet.payload);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -15,6 +15,11 @@ class AirQuality(Enum):
|
||||
Poor = "Poor"
|
||||
Unknown = "Unknown"
|
||||
|
||||
class ChargeState(Enum):
|
||||
Charging = "charging"
|
||||
NotCharging = "not-charging"
|
||||
Trickle = "trickle"
|
||||
|
||||
class FanMode(Enum):
|
||||
Auto = "Auto"
|
||||
Manual = "Manual"
|
||||
@@ -85,6 +90,7 @@ class ScryptedInterface(Enum):
|
||||
BufferConverter = "BufferConverter"
|
||||
CO2Sensor = "CO2Sensor"
|
||||
Camera = "Camera"
|
||||
Charger = "Charger"
|
||||
ColorSettingHsv = "ColorSettingHsv"
|
||||
ColorSettingRgb = "ColorSettingRgb"
|
||||
ColorSettingTemperature = "ColorSettingTemperature"
|
||||
@@ -750,6 +756,10 @@ class Camera:
|
||||
pass
|
||||
pass
|
||||
|
||||
class Charger:
|
||||
chargeState: ChargeState
|
||||
pass
|
||||
|
||||
class ColorSettingHsv:
|
||||
hsv: ColorHsv
|
||||
async def setHsv(self, hue: float, saturation: float, value: float) -> None:
|
||||
@@ -1342,6 +1352,7 @@ class ScryptedInterfaceProperty(Enum):
|
||||
lockState = "lockState"
|
||||
entryOpen = "entryOpen"
|
||||
batteryLevel = "batteryLevel"
|
||||
chargeState = "chargeState"
|
||||
online = "online"
|
||||
fromMimeType = "fromMimeType"
|
||||
toMimeType = "toMimeType"
|
||||
@@ -1618,6 +1629,13 @@ class DeviceState:
|
||||
def batteryLevel(self, value: float):
|
||||
self.setScryptedProperty("batteryLevel", value)
|
||||
|
||||
@property
|
||||
def chargeState(self) -> ChargeState:
|
||||
return self.getScryptedProperty("chargeState")
|
||||
@chargeState.setter
|
||||
def chargeState(self, value: ChargeState):
|
||||
self.setScryptedProperty("chargeState", value)
|
||||
|
||||
@property
|
||||
def online(self) -> bool:
|
||||
return self.getScryptedProperty("online")
|
||||
@@ -2097,6 +2115,13 @@ ScryptedInterfaceDescriptors = {
|
||||
"batteryLevel"
|
||||
]
|
||||
},
|
||||
"Charger": {
|
||||
"name": "Charger",
|
||||
"methods": [],
|
||||
"properties": [
|
||||
"chargeState"
|
||||
]
|
||||
},
|
||||
"Refresh": {
|
||||
"name": "Refresh",
|
||||
"methods": [
|
||||
|
||||
@@ -5,7 +5,7 @@ export type ScryptedNativeId = string | undefined;
|
||||
|
||||
/**
|
||||
* All devices in Scrypted implement ScryptedDevice, which contains the id, name, and type. Add listeners to subscribe to events from that device.
|
||||
*
|
||||
*
|
||||
* @category Core Reference
|
||||
*/
|
||||
export interface ScryptedDevice {
|
||||
@@ -78,7 +78,7 @@ export interface EventDetails {
|
||||
}
|
||||
/**
|
||||
* Returned when an event listener is attached to an EventEmitter. Call removeListener to unregister from events.
|
||||
*
|
||||
*
|
||||
* @category Core Reference
|
||||
*/
|
||||
export interface EventListenerRegister {
|
||||
@@ -230,7 +230,7 @@ export interface Notifier {
|
||||
}
|
||||
/**
|
||||
* MediaObject is an intermediate object within Scrypted to represent all media objects. Plugins should use the MediaConverter to convert the Scrypted MediaObject into a desired type, whether it is a externally accessible URL, a Buffer, etc.
|
||||
*
|
||||
*
|
||||
* @category Media Reference
|
||||
*/
|
||||
export interface MediaObject {
|
||||
@@ -560,7 +560,7 @@ export interface ResponseMediaStreamOptions extends MediaStreamOptions {
|
||||
|
||||
/**
|
||||
* Set this to true to allow for prebuffering even if the device implements the Battery interface.
|
||||
* Handy if you have a device that can continuously prebuffer when on mains power, but you still
|
||||
* Handy if you have a device that can continuously prebuffer when on mains power, but you still
|
||||
* want battery status reported.
|
||||
*/
|
||||
allowBatteryPrebuffer?: boolean;
|
||||
@@ -752,7 +752,7 @@ export interface VideoClips {
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercom devices can playback audio.
|
||||
* Intercom devices can playback audio.
|
||||
*/
|
||||
export interface Intercom {
|
||||
startIntercom(media: MediaObject): Promise<void>;
|
||||
@@ -874,7 +874,7 @@ export interface EntrySensor {
|
||||
}
|
||||
/**
|
||||
* DeviceManager is the interface used by DeviceProvider to report new devices, device states, and device events to Scrypted.
|
||||
*
|
||||
*
|
||||
* @category Device Provider Reference
|
||||
*/
|
||||
export interface DeviceManager {
|
||||
@@ -952,7 +952,7 @@ export interface DeviceManager {
|
||||
}
|
||||
/**
|
||||
* DeviceProvider acts as a controller/hub and exposes multiple devices to Scrypted Device Manager.
|
||||
*
|
||||
*
|
||||
* @category Device Provider Reference
|
||||
*/
|
||||
export interface DeviceProvider {
|
||||
@@ -970,7 +970,7 @@ export interface DeviceProvider {
|
||||
}
|
||||
/**
|
||||
* DeviceManifest is passed to DeviceManager.onDevicesChanged to sync a full list of devices from the controller/hub (Hue, SmartThings, etc)
|
||||
*
|
||||
*
|
||||
* @category Device Provider Reference
|
||||
*/
|
||||
export interface DeviceManifest {
|
||||
@@ -985,7 +985,7 @@ export interface DeviceCreatorSettings {
|
||||
}
|
||||
/**
|
||||
* A DeviceProvider that allows the user to create a device.
|
||||
*
|
||||
*
|
||||
* @category Device Provider Reference
|
||||
*/
|
||||
export interface DeviceCreator {
|
||||
@@ -1013,7 +1013,7 @@ export interface AdoptDevice {
|
||||
}
|
||||
/**
|
||||
* A DeviceProvider that has a device discovery mechanism.
|
||||
*
|
||||
*
|
||||
* @category Device Provider Reference
|
||||
*/
|
||||
export interface DeviceDiscovery {
|
||||
@@ -1035,9 +1035,21 @@ export interface DeviceDiscovery {
|
||||
export interface Battery {
|
||||
batteryLevel?: number;
|
||||
}
|
||||
export enum ChargeState {
|
||||
Trickle = 'trickle',
|
||||
Charging = 'charging',
|
||||
NotCharging = 'not-charging',
|
||||
}
|
||||
/**
|
||||
* Charger reports whether or not a device is being charged from an external power source.
|
||||
* Usually used for battery powered devices.
|
||||
*/
|
||||
export interface Charger {
|
||||
chargeState?: ChargeState;
|
||||
}
|
||||
/**
|
||||
* Refresh indicates that this device has properties that are not automatically updated, and must be periodically refreshed via polling. Device implementations should never implement their own underlying polling algorithm, and instead implement Refresh to allow Scrypted to manage polling intelligently.
|
||||
*
|
||||
*
|
||||
* @category Device Provider Reference
|
||||
*/
|
||||
export interface Refresh {
|
||||
@@ -1537,7 +1549,7 @@ export interface DeviceInformation {
|
||||
}
|
||||
/**
|
||||
* Device objects are created by DeviceProviders when new devices are discover and synced to Scrypted via the DeviceManager.
|
||||
*
|
||||
*
|
||||
* @category Device Provider Reference
|
||||
*/
|
||||
export interface Device {
|
||||
@@ -1563,7 +1575,7 @@ export interface EndpointAccessControlAllowOrigin {
|
||||
|
||||
/**
|
||||
* EndpointManager provides publicly accessible URLs that can be used to contact your Scrypted Plugin.
|
||||
*
|
||||
*
|
||||
* @category Webhook and Push Reference
|
||||
*/
|
||||
export interface EndpointManager {
|
||||
@@ -1656,7 +1668,7 @@ export interface EndpointManager {
|
||||
}
|
||||
/**
|
||||
* SystemManager is used by scripts to query device state and access devices.
|
||||
*
|
||||
*
|
||||
* @category Core Reference
|
||||
*/
|
||||
export interface SystemManager {
|
||||
@@ -1714,7 +1726,7 @@ export interface SystemManager {
|
||||
}
|
||||
/**
|
||||
* MixinProviders can add and intercept interfaces to other devices to add or augment their behavior.
|
||||
*
|
||||
*
|
||||
* @category Mixin Reference
|
||||
*/
|
||||
export interface MixinProvider {
|
||||
@@ -1735,7 +1747,7 @@ export interface MixinProvider {
|
||||
}
|
||||
/**
|
||||
* The HttpRequestHandler allows handling of web requests under the endpoint path: /endpoint/npm-package-name/*.
|
||||
*
|
||||
*
|
||||
* @category Webhook and Push Reference
|
||||
*/
|
||||
export interface HttpRequestHandler {
|
||||
@@ -1760,7 +1772,7 @@ export interface HttpRequest {
|
||||
}
|
||||
/**
|
||||
* Response object provided by the HttpRequestHandler.
|
||||
*
|
||||
*
|
||||
* @category Webhook and Push Reference
|
||||
*/
|
||||
export interface HttpResponse {
|
||||
@@ -1791,7 +1803,7 @@ export interface EngineIOHandler {
|
||||
}
|
||||
/**
|
||||
* @category Webhook and Push Reference
|
||||
*
|
||||
*
|
||||
*/
|
||||
export interface PushHandler {
|
||||
/**
|
||||
@@ -1885,6 +1897,7 @@ export enum ScryptedInterface {
|
||||
DeviceDiscovery = "DeviceDiscovery",
|
||||
DeviceCreator = "DeviceCreator",
|
||||
Battery = "Battery",
|
||||
Charger = "Charger",
|
||||
Refresh = "Refresh",
|
||||
MediaPlayer = "MediaPlayer",
|
||||
Online = "Online",
|
||||
@@ -1935,7 +1948,7 @@ export type RTCSignalingSendIceCandidate = (candidate: RTCIceCandidateInit) => P
|
||||
|
||||
/**
|
||||
* Implemented by WebRTC cameras to negotiate a peer connection session with Scrypted.
|
||||
*
|
||||
*
|
||||
* @category WebRTC Reference
|
||||
*/
|
||||
export interface RTCSignalingSession {
|
||||
@@ -1954,6 +1967,7 @@ export interface RTCSignalingOptions {
|
||||
*/
|
||||
offer?: RTCSessionDescriptionInit;
|
||||
requiresOffer?: boolean;
|
||||
requiresAnswer?: boolean;
|
||||
/**
|
||||
* Disables trickle ICE. All candidates must be sent in the initial offer/answer sdp.
|
||||
*/
|
||||
@@ -1979,7 +1993,7 @@ export interface RTCSignalingOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* A flexible RTC signaling endpoint, typically a browser, that can handle offer and answer.
|
||||
* A flexible RTC signaling endpoint, typically a browser, that can handle offer and answer.
|
||||
* Like Chromecast, etc, which has a Chromecast AppId that can connect to Scrypted.
|
||||
*/
|
||||
export interface RTCSignalingClient {
|
||||
@@ -2029,7 +2043,7 @@ export interface RTCConnectionManagement {
|
||||
* An inflexible RTC Signaling channel, typically a vendor, like Nest or Ring.
|
||||
* They generally can only handle either offer or answer, but not both. Usually has
|
||||
* strict requirements and expectations on client setup.
|
||||
*
|
||||
*
|
||||
* @category WebRTC Reference
|
||||
*/
|
||||
export interface RTCSignalingChannel {
|
||||
|
||||
@@ -10,3 +10,6 @@ scrypted.db.bak
|
||||
__pycache__
|
||||
.venv
|
||||
.vscode
|
||||
tsconfig.json
|
||||
test
|
||||
scripts
|
||||
|
||||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.7.42",
|
||||
"version": "0.7.46",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.7.42",
|
||||
"version": "0.7.46",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@mapbox/node-pre-gyp": "^1.0.10",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.7.43",
|
||||
"version": "0.7.46",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@mapbox/node-pre-gyp": "^1.0.10",
|
||||
@@ -72,8 +72,7 @@
|
||||
"prebeta": "npm version patch && git add package.json && npm run build && git commit -m prebeta",
|
||||
"beta": "npm publish --tag beta",
|
||||
"release": "npm publish",
|
||||
"prerelease": "npm version patch && git add package.json && npm run build && git commit -m prerelease",
|
||||
"postrelease": "git tag v$npm_package_version && git push origin v$npm_package_version",
|
||||
"postrelease": "git tag v$npm_package_version && git push origin v$npm_package_version && npm version patch && git add package.json && npm run build && git commit -m postrelease",
|
||||
"docker": "scripts/github-workflow-publish-docker.sh"
|
||||
},
|
||||
"author": "",
|
||||
|
||||
@@ -1,2 +1,9 @@
|
||||
const packageJson = require('../package.json');
|
||||
console.log(packageJson.version);
|
||||
async function main() {
|
||||
const response = await fetch('https://registry.npmjs.org/@scrypted/server');
|
||||
const json = await response.json();
|
||||
console.log(json['dist-tags'].latest);
|
||||
// const packageJson = require('../package.json');
|
||||
// console.log(packageJson.version);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
@@ -279,6 +279,15 @@ export class DeviceManagerImpl implements DeviceManager {
|
||||
}
|
||||
}
|
||||
|
||||
function toStorageString(value: any) {
|
||||
if (value === null)
|
||||
return 'null';
|
||||
if (value === undefined)
|
||||
return 'undefined;'
|
||||
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
class StorageImpl implements Storage {
|
||||
api: PluginAPI;
|
||||
[name: string]: any;
|
||||
@@ -293,17 +302,17 @@ class StorageImpl implements Storage {
|
||||
];
|
||||
private static indexedHandler: ProxyHandler<StorageImpl> = {
|
||||
get(target, property) {
|
||||
if (StorageImpl.allowedMethods.includes(property.toString())) {
|
||||
const prop = property.toString();
|
||||
const f = target[property.toString()];
|
||||
if (prop === 'length')
|
||||
const keyString = property.toString();
|
||||
if (StorageImpl.allowedMethods.includes(keyString)) {
|
||||
const f = target[keyString];
|
||||
if (keyString === 'length')
|
||||
return f;
|
||||
return f.bind(target);
|
||||
}
|
||||
return target.getItem(property.toString());
|
||||
return target.getItem(toStorageString(property));
|
||||
},
|
||||
set(target, property, value): boolean {
|
||||
target.setItem(property.toString(), value);
|
||||
target.setItem(toStorageString(property), value);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -351,6 +360,8 @@ class StorageImpl implements Storage {
|
||||
this.api.setStorage(this.nativeId, this.storage);
|
||||
}
|
||||
setItem(key: string, value: string): void {
|
||||
key = toStorageString(key);
|
||||
value = toStorageString(value);
|
||||
if (this.storage[this.prefix + key] === value)
|
||||
return;
|
||||
this.storage[this.prefix + key] = value;
|
||||
|
||||
@@ -94,6 +94,8 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
||||
super(app);
|
||||
this.datastore = datastore;
|
||||
this.app = app;
|
||||
// ensure that all the users are loaded from the db.
|
||||
this.usersService.getAllUsers();
|
||||
|
||||
this.pluginHosts.set('python', (_, pluginId, options) => new PythonRuntimeWorker(pluginId, options));
|
||||
this.pluginHosts.set('node', (mainFilename, pluginId, options) => new NodeForkWorker(mainFilename, pluginId, options));
|
||||
|
||||
@@ -212,7 +212,7 @@ async function start(mainFilename: string, options?: {
|
||||
const sha = hash.digest().toString('hex');
|
||||
|
||||
if (checkHash === sha) {
|
||||
const userToken = validateToken(tokenPart);
|
||||
const userToken = checkValidUserToken(tokenPart);
|
||||
if (userToken) {
|
||||
res.locals.username = userToken.username;
|
||||
res.locals.aclId = userToken.aclId;
|
||||
@@ -420,19 +420,23 @@ async function start(mainFilename: string, options?: {
|
||||
return req.secure ? 'login_user_token' : 'login_user_token_insecure';
|
||||
};
|
||||
|
||||
const validateToken = (token: string) => {
|
||||
const checkValidUserToken = (token: string) => {
|
||||
if (!token)
|
||||
return;
|
||||
try {
|
||||
return UserToken.validateToken(token);
|
||||
const userToken = UserToken.validateToken(token);
|
||||
if (scrypted.usersService.users.has(userToken.username))
|
||||
return userToken;
|
||||
}
|
||||
catch (e) {
|
||||
console.warn('invalid token', e.message);
|
||||
// console.warn('invalid token', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
const getSignedLoginUserTokenRawValue = (req: Request<any>) => req.signedCookies[getLoginUserToken(req)] as string;
|
||||
const getSignedLoginUserToken = (req: Request<any>) => validateToken(getSignedLoginUserTokenRawValue(req));
|
||||
const getSignedLoginUserToken = (req: Request<any>) => {
|
||||
const token = req.signedCookies[getLoginUserToken(req)] as string;
|
||||
return checkValidUserToken(token)
|
||||
};
|
||||
|
||||
app.get('/logout', (req, res) => {
|
||||
res.clearCookie(getLoginUserToken(req));
|
||||
@@ -449,11 +453,7 @@ async function start(mainFilename: string, options?: {
|
||||
if (process.env.SCRYPTED_ADMIN_USERNAME && process.env.SCRYPTED_ADMIN_TOKEN) {
|
||||
let user = await db.tryGet(ScryptedUser, process.env.SCRYPTED_ADMIN_USERNAME);
|
||||
if (!user) {
|
||||
user = new ScryptedUser();
|
||||
user._id = process.env.SCRYPTED_ADMIN_USERNAME;
|
||||
setScryptedUserPassword(user, crypto.randomBytes(8).toString('hex'), Date.now());
|
||||
user.token = crypto.randomBytes(16).toString('hex');
|
||||
await db.upsert(user);
|
||||
user = await scrypted.usersService.addUserInternal(process.env.SCRYPTED_ADMIN_USERNAME, crypto.randomBytes(8).toString('hex'), undefined);
|
||||
hasLogin = true;
|
||||
}
|
||||
}
|
||||
@@ -524,11 +524,7 @@ async function start(mainFilename: string, options?: {
|
||||
return;
|
||||
}
|
||||
|
||||
const user = new ScryptedUser();
|
||||
user._id = username;
|
||||
setScryptedUserPassword(user, password, timestamp);
|
||||
user.token = crypto.randomBytes(16).toString('hex');
|
||||
await db.upsert(user);
|
||||
const user = await scrypted.usersService.addUserInternal(username, password, undefined);
|
||||
hasLogin = true;
|
||||
|
||||
const userToken = new UserToken(username, user.aclId, timestamp);
|
||||
@@ -621,10 +617,9 @@ async function start(mainFilename: string, options?: {
|
||||
|
||||
// cookie auth
|
||||
try {
|
||||
const login_user_token = getSignedLoginUserTokenRawValue(req);
|
||||
if (!login_user_token)
|
||||
const userToken = getSignedLoginUserToken(req);
|
||||
if (!userToken)
|
||||
throw new Error('Not logged in.');
|
||||
const userToken = UserToken.validateToken(login_user_token);
|
||||
|
||||
res.send({
|
||||
...createTokens(userToken),
|
||||
|
||||
@@ -3,14 +3,32 @@ import { ScryptedRuntime } from "../runtime";
|
||||
import crypto from 'crypto';
|
||||
|
||||
export class UsersService {
|
||||
users = new Map<string, ScryptedUser>();
|
||||
usersPromise: Promise<ScryptedUser[]>;
|
||||
|
||||
constructor(public scrypted: ScryptedRuntime) {
|
||||
}
|
||||
|
||||
async getAllUsers() {
|
||||
const users: ScryptedUser[] = [];
|
||||
for await (const user of this.scrypted.datastore.getAll(ScryptedUser)) {
|
||||
users.push(user);
|
||||
private async ensureUsersPromise() {
|
||||
if (!this.usersPromise) {
|
||||
this.usersPromise = (async() => {
|
||||
const users = new Map<string, ScryptedUser>();
|
||||
for await (const user of this.scrypted.datastore.getAll(ScryptedUser)) {
|
||||
users.set(user._id, user);
|
||||
}
|
||||
this.users = users;
|
||||
return [...this.users.values()];
|
||||
})();
|
||||
}
|
||||
return this.usersPromise;
|
||||
}
|
||||
|
||||
private updateUsersPromise() {
|
||||
this.usersPromise = Promise.resolve([...this.users.values()]);
|
||||
}
|
||||
|
||||
async getAllUsers() {
|
||||
const users = await this.ensureUsersPromise();
|
||||
|
||||
return users.map(user => ({
|
||||
username: user._id,
|
||||
@@ -19,19 +37,38 @@ export class UsersService {
|
||||
}
|
||||
|
||||
async removeUser(username: string) {
|
||||
await this.ensureUsersPromise();
|
||||
|
||||
await this.scrypted.datastore.removeId(ScryptedUser, username);
|
||||
this.users.delete(username);
|
||||
this.updateUsersPromise();
|
||||
}
|
||||
|
||||
async removeAllUsers() {
|
||||
await this.ensureUsersPromise();
|
||||
|
||||
await this.scrypted.datastore.removeAll(ScryptedUser);
|
||||
this.users.clear();
|
||||
this.updateUsersPromise();
|
||||
}
|
||||
|
||||
async addUser(username: string, password: string, aclId: string) {
|
||||
async addUserInternal(username: string, password: string, aclId: string) {
|
||||
await this.ensureUsersPromise();
|
||||
|
||||
const user = new ScryptedUser();
|
||||
user._id = username;
|
||||
user.aclId = aclId;
|
||||
user.token = crypto.randomBytes(16).toString('hex');
|
||||
setScryptedUserPassword(user, password, Date.now());
|
||||
await this.scrypted.datastore.upsert(user);
|
||||
this.users.set(username, user);
|
||||
this.updateUsersPromise();
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async addUser(username: string, password: string, aclId: string) {
|
||||
await this.addUserInternal(username, password, aclId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ export class UserToken {
|
||||
}
|
||||
|
||||
static validateToken(token: string): UserToken {
|
||||
if (!token)
|
||||
throw new Error('Token not found.');
|
||||
|
||||
let json: {
|
||||
u: string,
|
||||
a: string,
|
||||
|
||||
Reference in New Issue
Block a user