diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..6eb251f Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ffe1890 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +node_modules/ + +plugin/authorized_admins.txt +plugin/telegram_users.json + +.env + +plugin/datasetModels/saved_datas +plugin/datasetModels/hourly_archive.json +plugin/datasetModels/logs_references.json \ No newline at end of file diff --git a/README.md b/README.md index e69de29..2c17438 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,2 @@ +In questa repository è presente il plugin MEB per SignalK. +Ulteriori informazioni verranno aggiunte nelle prossime versioni \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..bcbd776 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2596 @@ +{ + "name": "meb", + "version": "1.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "meb", + "version": "1.2.0", + "dependencies": { + "axios": "^1.12.2", + "dotenv": "^17.2.3", + "form-data": "^4.0.5", + "fs": "0.0.1-security", + "i": "^0.3.7", + "jsonwebtoken": "^9.0.2", + "node-telegram-bot-api": "^0.66.0", + "path": "^0.12.7" + } + }, + "node_modules/@cypress/request": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.9.tgz", + "integrity": "sha512-I3l7FdGRXluAS44/0NguwWlO83J18p0vlr2FYHrJkWdNYhgVoiYo61IXPqaOsL+vNxU1ZqMACzItGK3/KKDsdw==", + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~4.0.4", + "http-signature": "~1.4.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "6.14.0", + "safe-buffer": "^5.1.2", + "tough-cookie": "^5.0.0", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/request-promise": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@cypress/request-promise/-/request-promise-5.0.0.tgz", + "integrity": "sha512-eKdYVpa9cBEw2kTBlHeu1PP16Blwtum6QHg/u9s/MoHkZfuo1pRGka1VlUHXF5kdew82BvOJVVGk0x8X0nbp+w==", + "license": "ISC", + "dependencies": { + "bluebird": "^3.5.0", + "request-promise-core": "1.1.3", + "stealthy-require": "^1.1.1", + "tough-cookie": "^4.1.3" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "@cypress/request": "^3.0.0" + } + }, + "node_modules/@cypress/request-promise/node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findindex": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/array.prototype.findindex/-/array.prototype.findindex-2.2.4.tgz", + "integrity": "sha512-LLm4mhxa9v8j0A/RPnpQAP4svXToJFh+Hp1pNYl5ZD5qpB4zdx/D4YjpVcETkhFbUKWO3iGMVLvrOnnmkAJT6A==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT" + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT", + "peer": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT", + "peer": true + }, + "node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", + "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==", + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-signature": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", + "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.18.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/i": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/i/-/i-0.3.7.tgz", + "integrity": "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "license": "MIT" + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT", + "peer": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-telegram-bot-api": { + "version": "0.66.0", + "resolved": "https://registry.npmjs.org/node-telegram-bot-api/-/node-telegram-bot-api-0.66.0.tgz", + "integrity": "sha512-s4Hrg5q+VPl4/tJVG++pImxF6eb8tNJNj4KnDqAOKL6zGU34lo9RXmyAN158njwGN+v8hdNf8s9fWIYW9hPb5A==", + "license": "MIT", + "dependencies": { + "@cypress/request": "^3.0.1", + "@cypress/request-promise": "^5.0.0", + "array.prototype.findindex": "^2.0.2", + "bl": "^1.2.3", + "debug": "^3.2.7", + "eventemitter3": "^3.0.0", + "file-type": "^3.9.0", + "mime": "^1.6.0", + "pump": "^2.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/path": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "license": "MIT", + "dependencies": { + "process": "^0.11.1", + "util": "^0.10.3" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT" + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request-promise-core": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", + "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", + "license": "ISC", + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "request": "^2.34" + } + }, + "node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/request/node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/request/node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "license": "MIT", + "peer": true, + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/request/node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/request/node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "license": "MIT", + "peer": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==", + "license": "ISC", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "license": "MIT", + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT" + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d192fd8 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "meb", + "version": "1.5.0", + "description": "Il plugin personalizzato realizzato dal MEB per tener traccia dei log della barca, implementare previsioni meteo e molto altro.", + "main": "plugin/index.cjs", + "keywords": [ + "signalk-node-server-plugin", + "signalk-category-utility", + "signalk-plugin" + ], + "schema": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "title": "Abilita plugin", + "default": true + } + } + }, + "signalk-plugin-enabled-by-default": true, + "signalk": { + "displayName": "MEB" + }, + "dependencies": { + "axios": "^1.12.2", + "dotenv": "^17.2.3", + "form-data": "^4.0.5", + "fs": "0.0.1-security", + "i": "^0.3.7", + "jsonwebtoken": "^9.0.2", + "node-telegram-bot-api": "^0.66.0", + "path": "^0.12.7" + } +} diff --git a/plugin/.DS_Store b/plugin/.DS_Store new file mode 100644 index 0000000..1240d43 Binary files /dev/null and b/plugin/.DS_Store differ diff --git a/plugin/api_models/aisstream.js b/plugin/api_models/aisstream.js new file mode 100644 index 0000000..6310909 --- /dev/null +++ b/plugin/api_models/aisstream.js @@ -0,0 +1,32 @@ +const apiToken = "08a9a9828f8186c661d0293741fd01971bc2d2f4" + +function aisStream() { + + const socket = new WebSocket('wss://stream.aisstream.io/v0/stream'); + socket.onopen = function (_) { + let subscriptionMessage = { + Apikey: apiToken, + BoundingBox: [[15.0, 37.5], [16.5, 38.8]] + } + socket.send(JSON.stringify(subscriptionMessage)); + + console.log("✅ WebSocket Connected"); + }; + + socket.onmessage = function (event) { + event.data.text().then(text => { + try { + const json = JSON.parse(text); + console.log(json); + + } catch (e) { + console.error("Invalid JSON:", text); + } + }); + }; + + socket.onerror = (error) => console.error('WebSocket Error:', error); + socket.onclose = () => console.log('WebSocket Connection Closed'); +} + +module.exports = { aisStream }; \ No newline at end of file diff --git a/plugin/api_models/openmeteo.js b/plugin/api_models/openmeteo.js new file mode 100644 index 0000000..eb873cf --- /dev/null +++ b/plugin/api_models/openmeteo.js @@ -0,0 +1,212 @@ +const axios = require('axios'); + +const TIMEOUT = 10000; +const HEADERS = { Accept: "application/json, text/plain;q=0.9,*/*;q=0.8" }; + +// Parametri API +const FORECAST_PARAMS = { + current: [ + 'temperature_2m', + 'wind_speed_10m', + 'wind_direction_10m', + 'wind_gusts_10m', + 'precipitation', + 'rain', + 'relative_humidity_2m', + 'pressure_msl' + ], + hourly: [ + 'temperature_2m', + 'precipitation_probability', + 'precipitation', + 'rain', + 'wind_speed_10m', + 'cloud_cover', + 'wind_direction_10m', + 'relative_humidity_2m', + 'pressure_msl' + ] +}; + +const MARINE_PARAMS = { + current: [ + 'wave_height', + 'wave_direction', + 'wave_period', + 'wave_peak_period', + 'ocean_current_velocity', + 'ocean_current_direction' + ], + hourly: [ + 'wave_height', + 'wave_direction', + 'wave_period', + 'wave_peak_period', + 'ocean_current_velocity', + 'ocean_current_direction' + ] +}; + +// Unità di misura globali (aggiornate da OpenMeteo) +let globalUnits = { + forecast: { + temperature: '°C', + humidity: '%', + pressure: 'hPa', + windSpeed: 'km/h', + windDirection: '°', + windGusts: 'km/h', + rain: 'mm', + precipitation: 'mm' + }, + waves: { + waveHeight: 'm', + wavePeriod: 's', + waveDirection: '°', + wavePeakPeriod: 's' + } +}; + +/** + * Ottiene le unità di misura globali + */ +function getUnits() { + return globalUnits; +} + +/** + * Formatta un valore con la sua unità + */ +function formatWithUnit(value, unitKey, category = 'forecast') { + if (value === null || value === undefined) return 'n/d'; + const unit = globalUnits[category]?.[unitKey] || ''; + return `${value}${unit}`; +} + +async function getForecast(location) { + if (!location?.latitude || !location?.longitude) { + console.warn('[OpenMeteo] Coordinate non valide per forecast'); + return null; + } + + const currentParams = FORECAST_PARAMS.current.join(","); + const hourlyParams = FORECAST_PARAMS.hourly.join(","); + const api = `https://api.open-meteo.com/v1/forecast?latitude=${location.latitude}&longitude=${location.longitude}&hourly=${hourlyParams}¤t=${currentParams}`; + + try { + const response = await axios.get(api, { + headers: HEADERS, + timeout: TIMEOUT, + validateStatus: (status) => status === 200 + }); + + const { data } = response; + + if (!data?.current) { + console.warn('[OpenMeteo Forecast] Risposta senza dati current'); + return null; + } + + // Aggiorna unità globali da API response + if (data.current_units) { + globalUnits.forecast = { + temperature: data.current_units.temperature_2m || '°C', + humidity: data.current_units.relative_humidity_2m || '%', + pressure: data.current_units.pressure_msl || 'hPa', + windSpeed: data.current_units.wind_speed_10m || 'km/h', + windDirection: data.current_units.wind_direction_10m || '°', + windGusts: data.current_units.wind_gusts_10m || 'km/h', + rain: data.current_units.rain || 'mm', + precipitation: data.current_units.precipitation || 'mm' + }; + } + + return { + temperature: data.current.temperature_2m ?? null, + humidity: data.current.relative_humidity_2m ?? null, + pressure: data.current.pressure_msl ?? null, + windSpeed: data.current.wind_speed_10m ?? null, + windDirection: data.current.wind_direction_10m ?? null, + windGusts: data.current.wind_gusts_10m ?? null, + rain: data.current.rain ?? null, + precipitation: data.current.precipitation ?? null, + // Unità di misura + units: globalUnits.forecast, + // Dati orari per grafici + hourly: { + time: data.hourly?.time, + temperature: data.hourly?.temperature_2m, + humidity: data.hourly?.relative_humidity_2m, + windSpeed: data.hourly?.wind_speed_10m + }, + hourlyUnits: data.hourly_units || null + }; + } catch (error) { + console.error(`[OpenMeteo Forecast] Errore: ${error.message}`); + return null; + } +} + +async function getSeaConditions(location) { + if (!location?.latitude || !location?.longitude) { + console.warn('[OpenMeteo] Coordinate non valide per onde'); + return null; + } + + const currentParams = MARINE_PARAMS.current.join(","); + const hourlyParams = MARINE_PARAMS.hourly.join(","); + const api = `https://marine-api.open-meteo.com/v1/marine?latitude=${location.latitude}&longitude=${location.longitude}&hourly=${hourlyParams}¤t=${currentParams}&models=ecmwf_wam`; + + try { + const response = await axios.get(api, { + headers: HEADERS, + timeout: TIMEOUT, + validateStatus: (status) => status === 200 + }); + + const { data } = response; + + if (!data?.current) { + console.warn('[OpenMeteo Marine] Risposta senza dati current'); + return null; + } + + // Aggiorna unità globali da API response + if (data.current_units) { + globalUnits.waves = { + waveHeight: data.current_units.wave_height || 'm', + wavePeriod: data.current_units.wave_period || 's', + waveDirection: data.current_units.wave_direction || '°', + wavePeakPeriod: data.current_units.wave_peak_period || 's', + currentVelocity: data.current_units.ocean_current_velocity || 'm/s', + currentDirection: data.current_units.ocean_current_direction || '°' + }; + } + + return { + waveHeight: data.current.wave_height ?? null, + wavePeriod: data.current.wave_period ?? null, + waveDirection: data.current.wave_direction ?? null, + wavePeakPeriod: data.current.wave_peak_period ?? null, + currentDirection: data.current.ocean_current_direction ?? null, + currentVelocity: data.current.ocean_current_velocity ?? null, + // Unità di misura + units: globalUnits.waves, + // Dati orari per grafici + hourly: { + time: data.hourly?.time, + waveHeight: data.hourly?.wave_height, + wavePeriod: data.hourly?.wave_period, + waveDirection: data.hourly?.wave_direction, + currentDirection: data.hourly?.ocean_current_direction, + currentVelocity: data.hourly?.ocean_current_velocity + }, + hourlyUnits: data.hourly_units || null + }; + } catch (error) { + console.error(`[OpenMeteo Marine] Errore: ${error.message}`); + return null; + } +} + +module.exports = { getSeaConditions, getForecast, getUnits, formatWithUnit }; \ No newline at end of file diff --git a/plugin/bot/telegram.core.js b/plugin/bot/telegram.core.js new file mode 100644 index 0000000..bc51d8a --- /dev/null +++ b/plugin/bot/telegram.core.js @@ -0,0 +1,935 @@ +/** + * telegram.core.js - Bot Telegram ottimizzato per MEB SignalK + * Gestione utenti, comandi e live updates + */ + +const fs = require("fs"); +const path = require("path"); +const { + encrypt, + decrypt, + generateToken, + generateReadableToken, + encryptLog, + decryptLog, + loadSecureFile, + saveSecureFile +} = require("../tools/crypt"); + +const TelegramBot = require('node-telegram-bot-api'); + +function getSK(path) { + if (!app) return null; + const v = app.getSelfPath(path); + return v && v.value !== undefined && v.value !== null ? v.value : null; +} + +// ==================== INIZIALIZZAZIONE BOT ==================== +const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN; +let bot = null; + +function initBot() { + if (!BOT_TOKEN) { + console.warn("[Telegram] BOT_TOKEN non impostato: bot disabilitato."); + return null; + } + + // Riusa istanza esistente se disponibile + if (global.__meb_telegram_bot) { + bot = global.__meb_telegram_bot; + console.log("[Telegram] Riutilizzo istanza bot esistente"); + } else { + bot = new TelegramBot(BOT_TOKEN, { polling: true }); + global.__meb_telegram_bot = bot; + console.log("[Telegram] Nuova istanza bot creata"); + } + + // Registra handlers solo una volta + if (!global.__meb_telegram_handlers) { + global.__meb_telegram_handlers = true; + registerHandlers(); + console.log("[Telegram] Handlers registrati"); + } + + return bot; +} + +// Inizializza all'import +bot = initBot(); + +// ==================== CONFIGURAZIONE ==================== +const CONFIG = { + filesPerPage: 8, + liveUpdateInterval: 3000, + fileExpirationTime: 10 +}; + +const telegram_users_file = path.join(__dirname, "..", "telegram_users.json"); +const logs_references_file = path.join(__dirname, "..", "datasetModels/logs_references.json"); +const authorized_admins_file = path.join(__dirname, "..", "authorized_admins.txt"); + +let app = null; + +// Maps per gestione timer e stati +const liveParamIntervals = new Map(); +const keyExpirationTimers = new Map(); + +// ==================== GESTIONE FILE SENSIBILI ==================== + +function loadAuthorizedAdmins() { + try { + if (!fs.existsSync(authorized_admins_file)) { + return new Set(); + } + const content = fs.readFileSync(authorized_admins_file, 'utf8'); + const admins = content + .split('\n') + .map(line => line.trim()) + .filter(line => line && !line.startsWith('#')); + return new Set(admins); + } catch (error) { + console.error('[Telegram] Errore caricamento admin:', error.message); + return new Set(); + } +} + +function saveAuthorizedAdmins(admins) { + try { + const adminArray = Array.from(admins); + const content = '# Authorized Admin ChatIDs (one per line)\n' + adminArray.join('\n'); + fs.writeFileSync(authorized_admins_file, content, 'utf8'); + return true; + } catch (error) { + console.error('[Telegram] Errore salvataggio admin:', error.message); + return false; + } +} + +function isAdmin(chatID) { + const admins = loadAuthorizedAdmins(); + return admins.has(String(chatID)); +} + +function loadUsers() { + return loadSecureFile(telegram_users_file, []); +} + +function saveUsers(users) { + saveSecureFile(telegram_users_file, users); +} + +function loadLogsReferences() { + return loadSecureFile(logs_references_file, { references: [] }); +} + +function saveLogsReferences(data) { + saveSecureFile(logs_references_file, data); +} + +function isAuthenticated(chatID) { + const user = getUserByChatID(chatID); + return user && user.hasLoggedYet; +} + +function createNewUser(permissions = ["basic"]) { + const users = loadUsers(); + const newUser = { + token: generateReadableToken(24), + chatID: null, + isAuthorized: permissions, + hasLoggedYet: false + }; + users.push(newUser); + saveUsers(users); + return newUser; +} + +function login(token, chatID) { + const users = loadUsers(); + const userIDX = users.findIndex(u => u.token === token); + + if (userIDX === -1) { + throw new Error("Token non valido"); + } + + const user = users[userIDX]; + + if (user.hasLoggedYet && user.chatID && user.chatID !== String(chatID)) { + throw new Error("Questo token è già associato ad un altro account"); + } + + if (!user.hasLoggedYet) { + const newToken = generateReadableToken(32); + user.token = newToken; + user.hasLoggedYet = true; + user.chatID = String(chatID); + users[userIDX] = user; + saveUsers(users); + return { ...user, isFirstLogin: true, newToken }; + } + + user.chatID = String(chatID); + users[userIDX] = user; + saveUsers(users); + return { ...user, isFirstLogin: false }; +} + +function logout(chatID) { + const users = loadUsers(); + const userIDX = users.findIndex(u => u.chatID === String(chatID)); + + if (userIDX === -1) { + return null; + } + + saveUsers(users); + return users[userIDX]; +} + +function getUserWith(token) { + const users = loadUsers(); + return users.find(u => u.token === token); +} + +function getUserByChatID(chatID) { + const users = loadUsers(); + return users.find(u => u.chatID === String(chatID)); +} + +async function linkBot(appInstance) { + app = appInstance; + if (!bot) { + console.warn("[MEB TELEGRAM] linkBot chiamato senza TOKEN: ritorno null."); + return null; + } + return bot; +} + +function fetchFiles(chatId, page = 0) { + const logDirectory = path.join(__dirname, "..", "datasetModels/saved_datas"); + + try { + const logsData = loadLogsReferences(); + const registeredFiles = new Set((logsData.references || []).map(r => r.name)); + + const items = fs.readdirSync(logDirectory); + + const files = items.filter(item => { + const fullPath = path.join(logDirectory, item); + return fs.statSync(fullPath).isFile() && registeredFiles.has(item); + }); + + if (files.length === 0) { + bot.sendMessage(chatId, "📂 Non ci sono log salvati."); + return; + } + + const sortedFiles = files + .map(file => ({ + name: file, + time: fs.statSync(path.join(logDirectory, file)).mtime.getTime() + })) + .sort((a, b) => b.time - a.time) + .map(file => file.name); + + const totalPages = Math.ceil(sortedFiles.length / CONFIG.filesPerPage); + let currentPage = page; + if (currentPage < 0) currentPage = 0; + if (currentPage > totalPages - 1) currentPage = totalPages - 1; + + const startIdx = currentPage * CONFIG.filesPerPage; + const endIdx = startIdx + CONFIG.filesPerPage; + const pageFiles = sortedFiles.slice(startIdx, endIdx); + + const fileButtons = pageFiles.map(file => [ + { text: `📄 ${file}`, callback_data: `request_file_${file}` } + ]); + + const navigationButtons = []; + + if (totalPages > 1) { + const navRow = []; + if (currentPage > 0) { + navRow.push({ text: "←", callback_data: `page_${currentPage - 1}` }); + } + navRow.push({ text: `📖 ${currentPage + 1}/${totalPages}`, callback_data: `page_info` }); + if (currentPage < totalPages - 1) { + navRow.push({ text: "→", callback_data: `page_${currentPage + 1}` }); + } + navigationButtons.push(navRow); + } + + navigationButtons.push([{ text: "Annulla", callback_data: "dismiss" }]); + + bot.sendMessage(chatId, + `📥 *Logs di Bordo*\n` + + `Ogni file corrisponde ad una *sessione*. Seleziona un file per scaricarlo.\n` + + `⚠️ Avrai solo *10 secondi* per salvare file e chiave.`, + { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: [...fileButtons, ...navigationButtons] } + } + ); + + } catch (error) { + bot.sendMessage(chatId, `Errore lettura directory: ${error.message}`); + } +} + +function getCurrentPosition() { + if (!app) return null; + const position = app.getSelfPath('navigation.position'); + if (!position) return null; + return { + latitude: position.value.latitude, + longitude: position.value.longitude, + }; +} + +async function send(message) { + if (!bot) return; + const users = loadUsers(); + const loggedUsers = users.filter(u => u.hasLoggedYet && u.chatID); + + for (const user of loggedUsers) { + try { + await bot.sendMessage(user.chatID, message); + } catch (error) { + console.error(`[Telegram] Send error to ${user.chatID}:`, error.message); + } + } +} + +// ==================== RENDER FUNCTIONS ==================== + +function renderPositionText() { + if (!app) return "❌ App non disponibile"; + + const pos = app.getSelfPath('navigation.position')?.value; + const sog = getSK('navigation.speedOverGround'); + const cog = getSK('navigation.courseOverGroundTrue'); + const heading = getSK('navigation.headingTrue'); + + const lat = pos?.latitude?.toFixed(5) ?? "N/A"; + const lon = pos?.longitude?.toFixed(5) ?? "N/A"; + const speed = sog != null ? (sog * 1.94384).toFixed(1) : "N/A"; // m/s to knots + const course = cog != null ? (cog * 180 / Math.PI).toFixed(0) : "N/A"; // rad to deg + const headingDeg = heading != null ? (heading * 180 / Math.PI).toFixed(0) : "N/A"; + + return `📍 *Posizione & Velocità*\n\n` + + `Latitudine: \`${lat}\`\n` + + `Longitudine: \`${lon}\`\n` + + `SOG: ${speed} kn\n` + + `COG: ${course}°\n` + + `Heading: ${headingDeg}°`; +} + +function renderWindText() { + const speed = getSK('meb.wind.speed'); + const direction = getSK('meb.wind.direction'); + + return `🌬️ *Vento*\n\n` + + `Velocità: ${speed} km/h\n` + + `Direzione: ${direction}°\n`; +} + +function renderWavesText() { + + const height = getSK('meb.waves.height'); + const period = getSK('meb.waves.period'); + const dir = getSK('meb.waves.direction'); + + return `🌊 *Onde*\n\n` + + `Altezza: ${height} m\n` + + `Periodo: ${period} s\n` + + `Direzione: ${dir}°`; +} + +function renderForecastsText() { + const temp = getSK('meb.temperature'); + const humidity = getSK('meb.humidity'); + const pressure = getSK('meb.pressure'); + const rain = getSK('meb.precipitation'); + return `⛅️ *Previsioni Meteo*\n\n` + + `Temperatura: ${temp} °C\n` + + `Umidità: ${humidity} %\n` + + `Pressione: ${pressure} hPa\n`; +} + +function renderBatteriesText() { + + const voltage = getSK('electrical.batteries.traction.voltage'); + const current = getSK('electrical.batteries.traction.current'); + const soc = getSK('electrical.batteries.traction.stateOfCharge'); + const power = getSK('electrical.batteries.traction.power'); + + return `🔋 *Batterie*\n\n` + + `Tensione: ${voltage?.toFixed(1) ?? "N/A"} V\n` + + `Corrente: ${current?.toFixed(1) ?? "N/A"} A\n` + + `SOC: ${soc != null ? (soc * 100).toFixed(0) : "N/A"} %\n` + + `Potenza: ${power?.toFixed(0) ?? "N/A"} W`; +} + +function renderDashboardText() { + const posText = renderPositionText() + const windText = renderWindText() + const wavesText = renderWavesText() + const forecastText = renderForecastsText() + const battText = renderBatteriesText() + + return `📊 *Dashboard Completa*\n` + + `\n${posText}\n\n` + + `\n${forecastText}\n\n` + + `\n${windText}\n\n` + + `\n${wavesText}\n\n` + + `\n${battText}`; +} + +// ==================== REGISTRAZIONE HANDLERS ==================== + +function registerHandlers() { + if (!bot) return; + + // Handler: /start + bot.onText(/\/start/, (msg) => { + const chatId = msg.chat.id; + + if (isAuthenticated(chatId)) { + const menu = { + keyboard: [ + [{ text: "📊 Dashboard" }], + [{ text: "Parametri di Bordo" }], + [{ text: "File di Logs" }], + [{ text: "Genera un nuovo log" }], + [{ text: "Stato dei Log" }] + ], + resize_keyboard: true, + one_time_keyboard: false + }; + + bot.sendMessage(chatId, + "Benvenuto nel Data Console.\n" + + "• Visualizza i dati del computer di bordo\n" + + "• Ricevi aggiornamenti su parametri a scelta\n" + + "• Scarica i file di log della barca", + { parse_mode: 'Markdown', reply_markup: menu } + ); + } else { + bot.sendMessage(chatId, + "Benvenuto nel MEB Data Console!\n" + + "Per accedere ai dati è necessario un token di accesso.", + { parse_mode: 'Markdown' } + ); + + bot.sendMessage(chatId, "👤 Login", { + reply_markup: { + inline_keyboard: [ + [{ text: "❓ Come ottengo un token", callback_data: "token_login_question" }], + [{ text: "🔑 Ho un token", callback_data: "token_ready" }] + ] + }, + parse_mode: 'Markdown' + }); + } + }); + + // Menu testuale + bot.onText(/📊 Dashboard/, (msg) => { + const chatId = msg.chat.id; + if (!isAuthenticated(chatId)) { + bot.sendMessage(chatId, "Effettua prima il login con /login "); + return; + } + + const dashboardMsg = renderDashboardText(); + bot.sendMessage(chatId, dashboardMsg, { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [{ text: "🔄 Aggiorna", callback_data: "refresh_dashboard" }], + [{ text: "📡 Live (3s)", callback_data: "live_dashboard" }] + ] + } + }); + }); + + bot.onText(/File di Logs/, (msg) => { + const chatId = msg.chat.id; + if (!isAuthenticated(chatId)) { + bot.sendMessage(chatId, "Effettua prima il login con /login "); + return; + } + fetchFiles(chatId, 0); + }); + + bot.onText(/Parametri di Bordo/, (msg) => { + const chatId = msg.chat.id; + + if (!isAuthenticated(chatId)) { + bot.sendMessage(chatId, "Effettua il login con /login "); + return; + } + + bot.sendMessage(chatId, "*Parametri di Bordo*\nQui potrai visualizzare i parametri attuali del computer di bordo. Scegli il parametro che vuoi visualizzare dal menu qui sotto.", { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [{ text: "📊 Dashboard", callback_data: "get_dashboard" }], + [{ text: "⛅️ Meteo", callback_data: "get_forecasts" }], + [{ text: "📍 Posizione", callback_data: "get_position" }], + [{ text: "🌬️ Vento", callback_data: "get_wind" }], + [{ text: "🌊 Onde", callback_data: "get_waves" }], + [{ text: "🔋 Batterie", callback_data: "get_batteries" }], + [{ text: "Annulla", callback_data: "dismiss" }] + ] + } + }); + }); + + // Login + bot.onText(/\/login\s+(.+)/, (msg, match) => { + const chatID = msg.chat.id; + const token = (match && match[1] || "").trim(); + + if (!token) { + bot.sendMessage(chatID, "Inserisci il token: /login "); + return; + } + + try { + const result = login(token, chatID); + if (!result) { + bot.sendMessage(chatID, "Token non valido."); + return; + } + + if (result.isFirstLogin) { + bot.sendMessage(chatID, + `*Primo accesso completato!*\n\n` + + `Il tuo nuovo token permanente:\n\`${result.newToken}\`\n\n` + + `Salvalo! Non potrà essere usato da altri account.`, + { parse_mode: 'Markdown' } + ); + } else { + bot.sendMessage(chatID, "✅ Login effettuato!"); + } + + const menu = { + keyboard: [ + [{ text: "📊 Dashboard" }], + [{ text: "Parametri di Bordo" }], + [{ text: "File di Logs" }], + [{ text: "Genera un nuovo log" }], + [{ text: "Stato dei Log" }] + ], + resize_keyboard: true + }; + + bot.sendMessage(chatID, "Menu principale:", { reply_markup: menu }); + } catch (error) { + bot.sendMessage(chatID, `❌ ${error.message}`); + } + }); + + bot.onText(/\/logout/, (msg) => { + const chatID = msg.chat.id; + const user = logout(chatID); + if (!user) { + bot.sendMessage(chatID, "Non sei loggato."); + return; + } + bot.sendMessage(chatID, "Logout effettuato. Usa /login per rientrare."); + }); + + // Admin commands + bot.onText(/\/newuser(?:\s+(.*))?/, (msg, match) => { + const chatID = msg.chat.id; + + if (!isAdmin(chatID)) { + bot.sendMessage(chatID, "⛔ Non autorizzato."); + return; + } + + const permissionsArg = (match && match[1] || "").trim(); + const permissions = permissionsArg + ? permissionsArg.split(',').map(p => p.trim()).filter(p => p) + : ["basic"]; + + try { + const newUser = createNewUser(permissions); + bot.sendMessage(chatID, + `✅ *Nuovo utente creato*\n\nToken: \`${newUser.token}\``, + { parse_mode: 'Markdown' } + ); + } catch (error) { + bot.sendMessage(chatID, `❌ ${error.message}`); + } + }); + + bot.onText(/\/addadmin\s+(\d+)/, (msg, match) => { + const chatID = msg.chat.id; + const newAdminID = match && match[1]; + + if (!isAdmin(chatID)) { + bot.sendMessage(chatID, "⛔ Non autorizzato."); + return; + } + + const admins = loadAuthorizedAdmins(); + if (admins.has(newAdminID)) { + bot.sendMessage(chatID, "Già admin."); + return; + } + + admins.add(newAdminID); + saveAuthorizedAdmins(admins); + bot.sendMessage(chatID, `✅ Admin \`${newAdminID}\` aggiunto.`, { parse_mode: 'Markdown' }); + }); + + bot.onText(/\/removeadmin\s+(\d+)/, (msg, match) => { + const chatID = msg.chat.id; + const adminToRemove = match && match[1]; + + if (!isAdmin(chatID)) { + bot.sendMessage(chatID, "⛔ Non autorizzato."); + return; + } + + if (adminToRemove === String(chatID)) { + bot.sendMessage(chatID, "Non puoi rimuovere te stesso."); + return; + } + + const admins = loadAuthorizedAdmins(); + if (!admins.has(adminToRemove)) { + bot.sendMessage(chatID, "Non è admin."); + return; + } + + admins.delete(adminToRemove); + saveAuthorizedAdmins(admins); + bot.sendMessage(chatID, `✅ Admin \`${adminToRemove}\` rimosso.`, { parse_mode: 'Markdown' }); + }); + + bot.onText(/\/listusers/, (msg) => { + const chatID = msg.chat.id; + + if (!isAdmin(chatID)) { + bot.sendMessage(chatID, "⛔ Non autorizzato."); + return; + } + + const users = loadUsers(); + if (users.length === 0) { + bot.sendMessage(chatID, "Nessun utente."); + return; + } + + let message = `👥 *Utenti:* ${users.length}\n\n`; + users.forEach((user, idx) => { + const status = user.hasLoggedYet ? '✅' : '⏳'; + message += `${idx + 1}. ${status} \`${user.chatID || 'N/A'}\`\n`; + }); + + bot.sendMessage(chatID, message, { parse_mode: 'Markdown' }); + }); + + bot.onText(/\/mychatid/, (msg) => { + bot.sendMessage(msg.chat.id, `ChatID: \`${msg.chat.id}\``, { parse_mode: 'Markdown' }); + }); + + // Interval control + bot.onText(/\/changei\s+(log|api)\s+(\d+)/, (msg, match) => { + const chatID = msg.chat.id; + + if (!isAdmin(chatID)) { + bot.sendMessage(chatID, "⛔ Non autorizzato."); + return; + } + + const type = match[1]; + const seconds = parseInt(match[2], 10); + + if (isNaN(seconds) || seconds < 1) { + bot.sendMessage(chatID, "❌ Secondi non validi (min 1)."); + return; + } + + const newIntervalMs = seconds * 1000; + + // Debug: verifica stato app + if (!app) { + bot.sendMessage(chatID, "❌ App non inizializzata. Riprova tra qualche secondo."); + console.error('[Telegram] app è null in change_interval'); + return; + } + + if (!app.intervalControl) { + bot.sendMessage(chatID, "❌ Sistema intervalControl non disponibile. Il plugin potrebbe non essere ancora avviato."); + console.error('[Telegram] app.intervalControl non esiste'); + return; + } + + try { + const result = app.intervalControl.updateInterval(type, newIntervalMs); + + if (result) { + const typeLabel = type === 'log' ? 'Log recording' : 'OpenMeteo API'; + bot.sendMessage(chatID, + `✅ *${typeLabel}* aggiornato a *${seconds}s*`, + { parse_mode: 'Markdown' } + ); + } else { + bot.sendMessage(chatID, "❌ Tipo non valido. Usa: `log` o `api`", { parse_mode: 'Markdown' }); + } + } catch (error) { + console.error('[Telegram] Errore change_interval:', error); + bot.sendMessage(chatID, `❌ Errore: ${error.message}`); + } + }); + + bot.onText(/\/intervals/, (msg) => { + const chatID = msg.chat.id; + + if (!isAdmin(chatID)) { + bot.sendMessage(chatID, "⛔ Non autorizzato."); + return; + } + + if (!app) { + bot.sendMessage(chatID, "❌ App non inizializzata."); + return; + } + + if (!app.intervalControl) { + bot.sendMessage(chatID, "❌ Sistema intervalControl non disponibile."); + return; + } + + try { + const intervals = app.intervalControl.getIntervals(); + + bot.sendMessage(chatID, + `⏱️ *Intervalli Attuali*\n\n` + + `📝 Log: *${intervals.log_interval / 1000}s*\n` + + `🌤️ API: *${intervals.openmeteo_interval / 1000}s*\n\n` + + `Per modificare:\n` + + `\`/changei log \`\n` + + `\`/changei api \``, + { parse_mode: 'Markdown' } + ); + } catch (error) { + console.error('[Telegram] Errore intervals:', error); + bot.sendMessage(chatID, `❌ Errore: ${error.message}`); + } + }); + + // Callback query handler + bot.on('callback_query', async (query) => { + const chatId = query.message.chat.id; + const messageId = query.message.message_id; + const data = query.data; + + await bot.answerCallbackQuery(query.id); + + if (!isAuthenticated(chatId) && !['token_login_question', 'token_ready'].includes(data)) { + bot.sendMessage(chatId, "Effettua prima il login."); + return; + } + + switch (data) { + case 'dismiss': + bot.deleteMessage(chatId, messageId).catch(() => {}); + break; + + case 'token_login_question': + bot.sendMessage(chatId, + "Per ottenere un token, contatta un amministratore del sistema." + ); + break; + + case 'token_ready': + bot.sendMessage(chatId, "Usa: /login "); + break; + + case 'get_dashboard': + case 'refresh_dashboard': + bot.editMessageText(renderDashboardText(), { + chat_id: chatId, + message_id: messageId, + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [{ text: "🔄 Aggiorna", callback_data: "refresh_dashboard" }], + [{ text: "📡 Live (3s)", callback_data: "live_dashboard" }], + [{ text: "⏹️ Chiudi", callback_data: "dismiss" }] + ] + } + }).catch(() => {}); + break; + + case 'live_dashboard': + // Ferma eventuali live precedenti + if (liveParamIntervals.has(chatId)) { + clearInterval(liveParamIntervals.get(chatId)); + } + + const interval = setInterval(() => { + bot.editMessageText(renderDashboardText(), { + chat_id: chatId, + message_id: messageId, + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [{ text: "⏹️ Stop Live", callback_data: "stop_live" }] + ] + } + }).catch(() => { + clearInterval(interval); + liveParamIntervals.delete(chatId); + }); + }, CONFIG.liveUpdateInterval); + + liveParamIntervals.set(chatId, interval); + break; + + case 'stop_live': + if (liveParamIntervals.has(chatId)) { + clearInterval(liveParamIntervals.get(chatId)); + liveParamIntervals.delete(chatId); + } + bot.editMessageText(renderDashboardText(), { + chat_id: chatId, + message_id: messageId, + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [{ text: "🔄 Aggiorna", callback_data: "refresh_dashboard" }], + [{ text: "📡 Live (3s)", callback_data: "live_dashboard" }], + [{ text: "⏹️ Chiudi", callback_data: "dismiss" }] + ] + } + }).catch(() => {}); + break; + + case 'get_position': + bot.editMessageText(renderPositionText(), { + chat_id: chatId, + message_id: messageId, + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: [[{ text: "← Indietro", callback_data: "back_to_params" }]] } + }).catch(() => {}); + break; + + case 'get_wind': + bot.editMessageText(renderWindText(), { + chat_id: chatId, + message_id: messageId, + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: [[{ text: "← Indietro", callback_data: "back_to_params" }]] } + }).catch(() => {}); + break; + + case 'get_waves': + bot.editMessageText(renderWavesText(), { + chat_id: chatId, + message_id: messageId, + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: [[{ text: "← Indietro", callback_data: "back_to_params" }]] } + }).catch(() => {}); + break; + + case 'get_forecasts': + bot.editMessageText(renderForecastsText(), { + chat_id: chatId, + message_id: messageId, + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: [[{ text: "← Indietro", callback_data: "back_to_params" }]] } + }).catch(() => {}); + break; + + case 'get_batteries': + bot.editMessageText(renderBatteriesText(), { + chat_id: chatId, + message_id: messageId, + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: [[{ text: "← Indietro", callback_data: "back_to_params" }]] } + }).catch(() => {}); + break; + + case 'back_to_params': + bot.editMessageText("*Parametri di Bordo*\nQui potrai visualizzare i parametri attuali del computer di bordo. Scegli il parametro che vuoi visualizzare dal menu qui sotto.", { + chat_id: chatId, + message_id: messageId, + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [{ text: "📊 Dashboard Completa", callback_data: "get_dashboard" }], + [{ text: "⛅️ Meteo", callback_data: "get_forecasts" }], + [{ text: "📍 Posizione", callback_data: "get_position" }], + [{ text: "🌬️ Vento", callback_data: "get_wind" }], + [{ text: "🌊 Onde", callback_data: "get_waves" }], + [{ text: "🔋 Batterie", callback_data: "get_batteries" }], + [{ text: "Annulla", callback_data: "dismiss" }] + ] + } + }).catch(() => {}); + break; + + default: + // Gestione paginazione file + if (data.startsWith('page_')) { + const page = parseInt(data.replace('page_', ''), 10); + if (!isNaN(page)) { + bot.deleteMessage(chatId, messageId).catch(() => {}); + fetchFiles(chatId, page); + } + } + // Gestione richiesta file + else if (data.startsWith('request_file_')) { + const fileName = data.replace('request_file_', ''); + const filePath = path.join(__dirname, "..", "datasetModels/saved_datas", fileName); + + if (fs.existsSync(filePath)) { + const logsData = loadLogsReferences(); + const fileRef = (logsData.references || []).find(r => r.name === fileName); + const key = fileRef?.key || "Chiave non trovata"; + + try { + const fileMsg = await bot.sendDocument(chatId, filePath, { + caption: `🔑 Chiave: \`${key}\`\n⚠️ Questo messaggio verrà eliminato tra 10 secondi.`, + parse_mode: 'Markdown' + }); + + // Elimina dopo 10 secondi + setTimeout(() => { + bot.deleteMessage(chatId, fileMsg.message_id).catch(() => {}); + }, CONFIG.fileExpirationTime * 1000); + + } catch (error) { + bot.sendMessage(chatId, `❌ Errore invio file: ${error.message}`); + } + } else { + bot.sendMessage(chatId, "❌ File non trovato."); + } + + bot.deleteMessage(chatId, messageId).catch(() => {}); + } + break; + } + }); + +} // Fine registerHandlers + +module.exports = { + linkBot, + send, + loadUsers, + saveUsers, + getUserByChatID, + isAuthenticated, + isAdmin +}; + + + diff --git a/plugin/bot/telegram_users.json b/plugin/bot/telegram_users.json new file mode 100644 index 0000000..9640e5d --- /dev/null +++ b/plugin/bot/telegram_users.json @@ -0,0 +1,24 @@ +[ + { + "token": "eccef678c73b825fd2af7a3ce76603aeef68c6280862f1c2", + "hasLogged": true, + "chatId": 5868470977, + "chatID": 5868470977 + }, + { + "token": "5A6MjMd6amSGgZbk6PZ9T9sdJKjWwbHM", + "chatID": "5868470977", + "isAuthorized": [ + "basic" + ], + "hasLoggedYet": true + }, + { + "token": "af9aBSY9taEedmZXFhy3Fhns3VHtXSxT", + "chatID": "838642766", + "isAuthorized": [ + "basic" + ], + "hasLoggedYet": true + } +] \ No newline at end of file diff --git a/plugin/config.js b/plugin/config.js new file mode 100644 index 0000000..8d1aafa --- /dev/null +++ b/plugin/config.js @@ -0,0 +1,11 @@ +const dotenv = require("dotenv"); +const path = require("path"); + +// Carica il file .env dalla root del plugin +dotenv.config({ path: path.resolve(__dirname, "..", ".env"), quiet: true }); + +const config = { + telegramBotToken: process.env.TELEGRAM_BOT_TOKEN, +}; + +module.exports = { config }; \ No newline at end of file diff --git a/plugin/datasetModels/datasetCore.js b/plugin/datasetModels/datasetCore.js new file mode 100644 index 0000000..c2249ca --- /dev/null +++ b/plugin/datasetModels/datasetCore.js @@ -0,0 +1,105 @@ +const fs = require("fs"); +const path = require("path"); + +// Coda di scrittura per gestire backpressure +let writeQueue = []; +let isDraining = false; + +/** + * Inizializza il dataset e lo prepara per essere salvato. + * + * @param {String[]} headers Un array di stringhe che rappresentano i tipi di dati. + * @param {WriteStream} streamer Lo stream di scrittura del file. + * @returns {boolean} True se l'inizializzazione ha successo + */ +function datasetInit(headers, streamer) { + if (!streamer || streamer.destroyed) { + console.error('[DatasetCore] Stream non valido per inizializzazione'); + return false; + } + if (!Array.isArray(headers) || headers.length === 0) { + console.error('[DatasetCore] Headers non validi'); + return false; + } + + writeQueue = []; + isDraining = false; + + streamer.write(headers.join(',') + '\n'); + return true; +} + +/** + * Aggiunge una riga di dati al dataset con gestione backpressure. + * + * @param {Object} data I dati da scrivere + * @param {String[]} headers Gli header delle colonne + * @param {WriteStream} streamer Lo stream di scrittura + * @returns {boolean} True se la scrittura è andata a buon fine + */ +function appendData(data, headers, streamer) { + if (!streamer || streamer.destroyed) { + console.error('[DatasetCore] Stream non disponibile o distrutto'); + return false; + } + if (!data || typeof data !== 'object') { + console.warn('[DatasetCore] Dati non validi, skip scrittura'); + return false; + } + + // Escape valori che contengono virgole o newline per CSV valido + const escapeCSV = (val) => { + if (val === undefined || val === null) return ''; + const str = String(val); + if (str.includes(',') || str.includes('\n') || str.includes('"')) { + return `"${str.replace(/"/g, '""')}"`; + } + return str; + }; + + const row = headers.map(header => escapeCSV(data[header])).join(','); + + // Gestione backpressure con coda + const canWrite = streamer.write(row + '\n'); + + if (!canWrite) { + if (!isDraining) { + isDraining = true; + console.warn('[DatasetCore] Buffer saturo, attendo drain...'); + streamer.once('drain', () => { + isDraining = false; + // Processa coda pendente + while (writeQueue.length > 0 && !streamer.destroyed) { + const pendingRow = writeQueue.shift(); + if (!streamer.write(pendingRow + '\n')) { + writeQueue.unshift(pendingRow); + break; + } + } + }); + } + // Aggiungi alla coda solo se non troppo piena (max 1000 entries) + if (writeQueue.length < 1000) { + writeQueue.push(row); + } else { + console.error('[DatasetCore] Coda piena, scarto dati'); + return false; + } + } + + return true; +} + +/** + * Ottiene la dimensione della coda di scrittura pendente + * @returns {number} Numero di righe in attesa + */ +function getPendingWrites() { + return writeQueue.length; +} + +module.exports = { + datasetInit, + appendData, + getPendingWrites +}; \ No newline at end of file diff --git a/plugin/datasetModels/datasetUtils.js b/plugin/datasetModels/datasetUtils.js new file mode 100644 index 0000000..0577ba7 --- /dev/null +++ b/plugin/datasetModels/datasetUtils.js @@ -0,0 +1,274 @@ +const fs = require('fs'); +const path = require('path'); + +/** + * Searches for a directory. If not found, creates it. + * @param {string} dirPath - The absolute or relative path to the directory. + * @returns {string} - The absolute path to the directory. + */ +function getDirectory(dirPath) { + const absolutePath = path.resolve(dirPath); + if (!fs.existsSync(absolutePath)) { + fs.mkdirSync(absolutePath, { recursive: true }); + } + return absolutePath; +} + +/** + * Searches for a file. If not found, creates it with initialData. + * @param {string} filePath - The absolute or relative path to the file. + * @param {object} [initialData={}] - The initial data to write if the file is created. + * @returns {object} - The content of the file as an object. + */ +function write(filePath, initialData = {}) { + const absolutePath = path.resolve(filePath); + const dir = path.dirname(absolutePath); + + getDirectory(dir); + + if (!fs.existsSync(absolutePath)) { + fs.writeFileSync(absolutePath, JSON.stringify(initialData, null, 2), 'utf-8'); + return initialData; + } + + try { + const content = fs.readFileSync(absolutePath, 'utf-8'); + return JSON.parse(content); + } catch (error) { + console.error(`Error reading or parsing JSON file at ${absolutePath}:`, error); + throw error; + } +} + +/** + * Scrive dati in un file JSON. + * @param {string} filePath - Il path assoluto o relativo al file JSON. + * @param {object} data - Gli elementi da aggiungere nel file JSON. + */ +function update(filePath, data) { + const absolutePath = path.resolve(filePath); + const dir = path.dirname(absolutePath); + + getDirectory(dir); + + fs.writeFileSync(absolutePath, JSON.stringify(data, null, 2), 'utf-8'); +} + +/** + * Aggiunge un elemento all'array del file specificato + * Se il file non esiste, lo crea con un array contenente l'elemento. + * Se il file esiste ma non è un array, genera un errore. + * @param {string} filePath - Il path del file JSON. + * @param {any} element - L'elemento da aggiungere all'array. + * @returns {array} - L'array aggiornato. + */ +function appendTo(filePath, element) { + const absolutePath = path.resolve(filePath); + let data = []; + + if (fs.existsSync(absolutePath)) { + try { + const content = fs.readFileSync(absolutePath, 'utf-8'); + data = JSON.parse(content); + } catch (error) { + console.error(`Error reading or parsing JSON file at ${absolutePath}:`, error); + throw error; + } + } else { + // Ensure directory exists if we are creating the file + const dir = path.dirname(absolutePath); + getDirectory(dir); + } + + if (!Array.isArray(data)) { + throw new Error(`File at ${absolutePath} exists but is not a JSON array.`); + } + + data.push(element); + fs.writeFileSync(absolutePath, JSON.stringify(data, null, 2), 'utf-8'); + return data; +} + +/** + * Aggiunge un elemento a un array specifico all'interno di un oggetto JSON + * Es: JSON = {date: "now", elements: [], security: false} + * appendToElement(filePath, 'elements', {title: "", description: ""}) + * + * @param {string} filePath - Il path del file JSON. + * @param {string} arrayKey - La chiave dell'array nell'oggetto JSON (es: 'elements'). + * @param {any} element - L'elemento da aggiungere all'array specificato. + * @returns {boolean} - Se l'operazione è andata a buon fine, restituisce true. + */ +function appendToElement(filePath, arrayKey, element) { + const absolutePath = path.resolve(filePath); + let data = {}; + + if (fs.existsSync(absolutePath)) { + try { + const content = fs.readFileSync(absolutePath, 'utf-8'); + data = JSON.parse(content); + } catch (error) { + console.error(`Error reading or parsing JSON file at ${absolutePath}:`, error); + throw error; + } + } else { + const dir = path.dirname(absolutePath); + getDirectory(dir); + data = {}; + } + + if (!data.hasOwnProperty(arrayKey)) { + data[arrayKey] = []; + } + if (!Array.isArray(data[arrayKey])) { + throw new Error(`Property '${arrayKey}' in file at ${absolutePath} exists but is not an array.`); + } + + data[arrayKey].push(element); + + fs.writeFileSync(absolutePath, JSON.stringify(data, null, 2), 'utf-8'); + return true +} + + +/** + * Rimuove un elemento da un array specifico all'interno di un oggetto JSON + * cercando per proprietà "name" + * Es: JSON = {date: "now", elements: [{name: "item1"}, {name: "item2"}], security: false} + * removeFromElement(filePath, 'elements', 'item1') + * + * @param {string} filePath - Il path del file JSON. + * @param {string} arrayKey - La chiave dell'array nell'oggetto JSON (es: 'elements'). + * @param {string} nameToRemove - Il valore della proprietà "name" dell'elemento da rimuovere. + * @returns {object} - Oggetto con {success: boolean, removed: object|null, remaining: number} + */ +function removeFromElement(filePath, arrayKey, nameToRemove) { + const absolutePath = path.resolve(filePath); + let data = {}; + + if (fs.existsSync(absolutePath)) { + try { + const content = fs.readFileSync(absolutePath, 'utf-8'); + data = JSON.parse(content); + } catch (error) { + console.error(`Error reading or parsing JSON file at ${absolutePath}:`, error); + throw error; + } + } else { + throw new Error(`File at ${absolutePath} does not exist.`); + } + + if (!data.hasOwnProperty(arrayKey)) { + throw new Error(`Property '${arrayKey}' does not exist in file at ${absolutePath}.`); + } + if (!Array.isArray(data[arrayKey])) { + throw new Error(`Property '${arrayKey}' in file at ${absolutePath} is not an array.`); + } + + const initialLength = data[arrayKey].length; + const indexToRemove = data[arrayKey].findIndex(item => item.name === nameToRemove); + + if (indexToRemove === -1) { + return { + success: false, + removed: null, + remaining: initialLength, + message: `Element with name '${nameToRemove}' not found in array '${arrayKey}'.` + }; + } + + const removedElement = data[arrayKey].splice(indexToRemove, 1)[0]; + fs.writeFileSync(absolutePath, JSON.stringify(data, null, 2), 'utf-8'); + + return true +} + + +function findInElement(filePath, arrayKey, name) { + const absolutePath = path.resolve(filePath); + let data = {}; + + if (fs.existsSync(absolutePath)) { + try { + const content = fs.readFileSync(absolutePath, 'utf-8'); + data = JSON.parse(content); + } catch (error) { + console.error(`Error reading or parsing JSON file at ${absolutePath}:`, error); + throw error; + } + } else { + throw new Error(`File at ${absolutePath} does not exist.`); + } + + if (!data.hasOwnProperty(arrayKey)) { + throw new Error(`Property '${arrayKey}' does not exist in file at ${absolutePath}.`); + } + if (!Array.isArray(data[arrayKey])) { + throw new Error(`Property '${arrayKey}' in file at ${absolutePath} is not an array.`); + } + + const index = data[arrayKey].findIndex(item => item.name === name); + + return data[arrayKey][index] +} + + +/** + * Aggiorna un elemento in un array specifico all'interno di un oggetto JSON + * cercando per proprietà "name" e sostituendolo con un nuovo elemento + * Es: JSON = {date: "now", elements: [{name: "item1", value: 10}, {name: "item2", value: 20}]} + * updateInElement(filePath, 'elements', 'item1', {name: "item1", value: 99}) + * + * @param {string} filePath - Il path del file JSON. + * @param {string} arrayKey - La chiave dell'array nell'oggetto JSON (es: 'elements'). + * @param {string} nameToUpdate - Il valore della proprietà "name" dell'elemento da aggiornare. + * @param {any} newElement - Il nuovo elemento che sostituirà quello trovato. + * @returns {boolean} - True se l'operazione ha successo, false se l'elemento non è stato trovato. + */ +function updateInElement(filePath, arrayKey, nameToUpdate, newElement) { + const absolutePath = path.resolve(filePath); + let data = {}; + + if (fs.existsSync(absolutePath)) { + try { + const content = fs.readFileSync(absolutePath, 'utf-8'); + data = JSON.parse(content); + } catch (error) { + console.error(`Error reading or parsing JSON file at ${absolutePath}:`, error); + throw error; + } + } else { + throw new Error(`File at ${absolutePath} does not exist.`); + } + + if (!data.hasOwnProperty(arrayKey)) { + throw new Error(`Property '${arrayKey}' does not exist in file at ${absolutePath}.`); + } + if (!Array.isArray(data[arrayKey])) { + throw new Error(`Property '${arrayKey}' in file at ${absolutePath} is not an array.`); + } + + const index = data[arrayKey].findIndex(item => item.name === nameToUpdate); + + if (index === -1) { + return false; + } + + data[arrayKey][index] = newElement; + fs.writeFileSync(absolutePath, JSON.stringify(data, null, 2), 'utf-8'); + + return true; +} + + + + +module.exports = { + getDirectory, + write, + update, + appendToElement, + findInElement, + removeFromElement, + updateInElement +}; diff --git a/plugin/datasetModels/graphsCore.js b/plugin/datasetModels/graphsCore.js new file mode 100644 index 0000000..1391ce9 --- /dev/null +++ b/plugin/datasetModels/graphsCore.js @@ -0,0 +1,350 @@ +const fs = require('fs'); +const path = require('path'); + +const ARCHIVE_FILE = path.join(__dirname, 'hourly_archive.json'); + +// Cache dati OpenMeteo condivisi (evita chiamate duplicate) +let sharedWeatherData = { + forecast: null, + waves: null, + units: null, // Unità di misura globali + lastUpdate: null, + updateInterval: 2 * 60 * 1000 // 2 minuti +}; + +// Archivio dati orari +let hourlyArchive = { + temperature: [], + windSpeed: [], + windDirection: [], + waveHeight: [], + wavePeriod: [], + waveDirection: [], + humidity: [], + pressure: [] +}; + +/** + * Carica l'archivio da file + */ +function loadArchive() { + try { + if (fs.existsSync(ARCHIVE_FILE)) { + const data = fs.readFileSync(ARCHIVE_FILE, 'utf8'); + const parsed = JSON.parse(data); + // Valida struttura archivio + if (parsed && typeof parsed === 'object') { + hourlyArchive = { + temperature: Array.isArray(parsed.temperature) ? parsed.temperature : [], + windSpeed: Array.isArray(parsed.windSpeed) ? parsed.windSpeed : [], + windDirection: Array.isArray(parsed.windDirection) ? parsed.windDirection : [], + waveHeight: Array.isArray(parsed.waveHeight) ? parsed.waveHeight : [], + wavePeriod: Array.isArray(parsed.wavePeriod) ? parsed.wavePeriod : [], + waveDirection: Array.isArray(parsed.waveDirection) ? parsed.waveDirection : [], + humidity: Array.isArray(parsed.humidity) ? parsed.humidity : [], + pressure: Array.isArray(parsed.pressure) ? parsed.pressure : [] + }; + console.log('[GraphsCore] Archivio caricato'); + } + } + } catch (error) { + console.error('[GraphsCore] Errore caricamento archivio:', error.message); + // Resetta archivio se corrotto + hourlyArchive = { + temperature: [], windSpeed: [], windDirection: [], + waveHeight: [], wavePeriod: [], waveDirection: [], + humidity: [], pressure: [] + }; + } +} + +/** + * Salva l'archivio su file + */ +function saveArchive() { + try { + fs.writeFileSync(ARCHIVE_FILE, JSON.stringify(hourlyArchive, null, 2)); + } catch (error) { + console.error('[GraphsCore] Errore salvataggio archivio:', error.message); + } +} + +/** + * Aggiorna i dati meteo condivisi + * @param {object} forecastData - Dati forecast da OpenMeteo + * @param {object} wavesData - Dati onde da OpenMeteo + */ +function updateSharedWeatherData(forecastData, wavesData) { + if (forecastData) { + sharedWeatherData.forecast = forecastData; + } + if (wavesData) { + sharedWeatherData.waves = wavesData; + } + + // Aggiorna unità se disponibili + if (forecastData?.units || wavesData?.units) { + sharedWeatherData.units = { + forecast: forecastData?.units || sharedWeatherData.units?.forecast || {}, + waves: wavesData?.units || sharedWeatherData.units?.waves || {} + }; + } + + sharedWeatherData.lastUpdate = Date.now(); +} + +/** + * Ottiene i dati meteo condivisi + * @returns {object} Dati meteo attuali + */ +function getSharedWeatherData() { + return { + forecast: sharedWeatherData.forecast, + waves: sharedWeatherData.waves, + units: sharedWeatherData.units, + lastUpdate: sharedWeatherData.lastUpdate, + isValid: sharedWeatherData.lastUpdate && + (Date.now() - sharedWeatherData.lastUpdate) < sharedWeatherData.updateInterval * 2 + }; +} + +/** + * Ottiene le unità di misura globali + */ +function getUnits() { + return sharedWeatherData.units || { + forecast: { + temperature: '°C', + humidity: '%', + pressure: 'hPa', + windSpeed: 'km/h', + windDirection: '°' + }, + waves: { + waveHeight: 'm', + wavePeriod: 's', + waveDirection: '°' + } + }; +} + +/** + * Formatta un valore con la sua unità + */ +function formatValue(value, unitKey, category = 'forecast') { + if (value === null || value === undefined) return 'n/d'; + const units = getUnits(); + const unit = units[category]?.[unitKey] || ''; + return `${value}${unit}`; +} + +/** + * Verifica se i dati condivisi sono ancora validi + */ +function isWeatherDataValid() { + if (!sharedWeatherData.lastUpdate) return false; + return (Date.now() - sharedWeatherData.lastUpdate) < sharedWeatherData.updateInterval; +} + +/** + * Archivia un punto dati orario + */ +function archiveHourlyData(data) { + if (!data || typeof data !== 'object') { + console.warn('[GraphsCore] archiveHourlyData: dati non validi'); + return; + } + + const timestamp = new Date().toISOString(); + const maxPoints = 168; // 7 giorni di dati orari + + const addPoint = (arr, value) => { + if (value === null || value === undefined || Number.isNaN(value)) return; + arr.push({ timestamp, value }); + if (arr.length > maxPoints) arr.shift(); + }; + + addPoint(hourlyArchive.temperature, data.temperature); + addPoint(hourlyArchive.windSpeed, data.windSpeed); + addPoint(hourlyArchive.windDirection, data.windDirection); + addPoint(hourlyArchive.waveHeight, data.waveHeight); + addPoint(hourlyArchive.wavePeriod, data.wavePeriod); + addPoint(hourlyArchive.waveDirection, data.waveDirection); + addPoint(hourlyArchive.humidity, data.humidity); + addPoint(hourlyArchive.pressure, data.pressure); + + saveArchive(); + console.log('[GraphsCore] Dati orari archiviati'); +} + +/** + * Ottiene i dati per un grafico specifico + * @param {string} parameter - temperatura, vento, onde, etc. + * @param {number} hours - ultimi N ore (default 24) + */ +function getGraphData(parameter, hours = 24) { + const paramMap = { + 'temperature': hourlyArchive.temperature, + 'windSpeed': hourlyArchive.windSpeed, + 'windDirection': hourlyArchive.windDirection, + 'waveHeight': hourlyArchive.waveHeight, + 'wavePeriod': hourlyArchive.wavePeriod, + 'waveDirection': hourlyArchive.waveDirection, + 'humidity': hourlyArchive.humidity, + 'pressure': hourlyArchive.pressure + }; + + const data = paramMap[parameter] || []; + const cutoff = Date.now() - (hours * 60 * 60 * 1000); + + return data.filter(point => new Date(point.timestamp).getTime() > cutoff); +} + +/** + * Genera dati formattati per Chart.js + */ +function formatForChart(parameter, hours = 24) { + const data = getGraphData(parameter, hours); + + return { + labels: data.map(p => { + const d = new Date(p.timestamp); + return `${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`; + }), + datasets: [{ + label: getParameterLabel(parameter), + data: data.map(p => p.value), + borderColor: getParameterColor(parameter), + backgroundColor: getParameterColor(parameter, 0.2), + tension: 0.3, + fill: true + }] + }; +} + +/** + * Label leggibili per i parametri + */ +function getParameterLabel(param) { + const labels = { + 'temperature': 'Temperatura (°C)', + 'windSpeed': 'Velocità Vento (km/h)', + 'windDirection': 'Direzione Vento (°)', + 'waveHeight': 'Altezza Onde (m)', + 'wavePeriod': 'Periodo Onde (s)', + 'waveDirection': 'Direzione Onde (°)', + 'humidity': 'Umidità (%)', + 'pressure': 'Pressione (hPa)' + }; + return labels[param] || param; +} + +/** + * Colori per i grafici + */ +function getParameterColor(param, alpha = 1) { + const colors = { + 'temperature': `rgba(255, 99, 132, ${alpha})`, + 'windSpeed': `rgba(54, 162, 235, ${alpha})`, + 'windDirection': `rgba(75, 192, 192, ${alpha})`, + 'waveHeight': `rgba(153, 102, 255, ${alpha})`, + 'wavePeriod': `rgba(255, 159, 64, ${alpha})`, + 'waveDirection': `rgba(255, 205, 86, ${alpha})`, + 'humidity': `rgba(201, 203, 207, ${alpha})`, + 'pressure': `rgba(100, 149, 237, ${alpha})` + }; + return colors[param] || `rgba(128, 128, 128, ${alpha})`; +} + +/** + * Ottiene tutti i dati disponibili per dashboard + */ +function getAllGraphsData(hours = 24) { + return { + temperature: formatForChart('temperature', hours), + windSpeed: formatForChart('windSpeed', hours), + waveHeight: formatForChart('waveHeight', hours), + humidity: formatForChart('humidity', hours) + }; +} + +/** + * Statistiche sull'archivio + */ +function getArchiveStats() { + return { + temperature: hourlyArchive.temperature.length, + windSpeed: hourlyArchive.windSpeed.length, + waveHeight: hourlyArchive.waveHeight.length, + oldestData: getOldestTimestamp(), + newestData: getNewestTimestamp() + }; +} + +function getOldestTimestamp() { + const all = [ + ...hourlyArchive.temperature, + ...hourlyArchive.windSpeed, + ...hourlyArchive.waveHeight + ]; + if (all.length === 0) return null; + return all.reduce((oldest, p) => + new Date(p.timestamp) < new Date(oldest.timestamp) ? p : oldest + ).timestamp; +} + +function getNewestTimestamp() { + const all = [ + ...hourlyArchive.temperature, + ...hourlyArchive.windSpeed, + ...hourlyArchive.waveHeight + ]; + if (all.length === 0) return null; + return all.reduce((newest, p) => + new Date(p.timestamp) > new Date(newest.timestamp) ? p : newest + ).timestamp; +} + +/** + * Pulisce l'archivio + */ +function clearArchive() { + hourlyArchive = { + temperature: [], + windSpeed: [], + windDirection: [], + waveHeight: [], + wavePeriod: [], + waveDirection: [], + humidity: [], + pressure: [] + }; + saveArchive(); + console.log('[GraphsCore] Archivio pulito'); +} + +// Carica archivio all'avvio +loadArchive(); + +module.exports = { + // Gestione dati condivisi + updateSharedWeatherData, + getSharedWeatherData, + isWeatherDataValid, + + // Unità di misura + getUnits, + formatValue, + + // Archivio orario + archiveHourlyData, + getGraphData, + formatForChart, + getAllGraphsData, + getArchiveStats, + clearArchive, + + // Utility + getParameterLabel, + getParameterColor +}; diff --git a/plugin/index.cjs b/plugin/index.cjs new file mode 100644 index 0000000..c6d7b83 --- /dev/null +++ b/plugin/index.cjs @@ -0,0 +1,596 @@ +const { config } = require("./config.js"); +const { setupRoutes, getOpenApiSpec } = require("./tools/routes.js"); +const { aisStream } = require("./api_models/aisstream.js") +const mapHandler = require("./tools/map.handler.js"); +const { linkBot, send } = require("./bot/telegram.core.js"); +const dataset = require("./datasetModels/datasetCore.js"); +const dataUtils = require("./datasetModels/datasetUtils.js"); +const graphsCore = require("./datasetModels/graphsCore.js"); +const { generateToken, encryptLog, loadSecureFile, saveSecureFile } = require("./tools/crypt.js"); +const fs = require("fs"); +const path = require("path"); +const { getForecast, getSeaConditions } = require("./api_models/openmeteo.js"); + +const { publish } = require("./tools/publisher.js"); + +// CONFIG modificabile runtime (non più frozen per permettere modifiche admin) +const CONFIG = { + log_interval: 2000, // Dataset entry ogni 2 secondi + openmeteo_interval: 300000, // OpenMeteo ogni 5 minuti + hourly_archive_interval: 3600000, // Archivio orario per grafici + number_value_fallback: 999999999999, + value_fallback: "Funzionalità da Sviluppare" +}; + +// Funzione per aggiornare gli intervalli runtime +function updateInterval(type, newIntervalMs) { + if (type === 'api' || type === 'openmeteo') { + CONFIG.openmeteo_interval = newIntervalMs; + return { type: 'openmeteo_interval', value: newIntervalMs }; + } else if (type === 'log') { + CONFIG.log_interval = newIntervalMs; + return { type: 'log_interval', value: newIntervalMs }; + } + return null; +} + +// Getter per CONFIG (usato da altri moduli) +function getConfig() { + return { ...CONFIG }; +} + +const CSV_HEADERS = Object.freeze([ + 'timestamp', + 'wavesHeight', + 'wavesPeriod', + 'wavesDirection', + 'windSpeed', + 'windDirection', + 'temperature', + // 'currentSpeed', + // 'currentDirection', + 'speedOverGround', + 'courseOverGround', + 'headingTrue', + 'latitude', + 'longitude', + '1Voltage', + '1Current', + '1StateOfCharge', + '1Temperature', + '0Voltage', + '0Current', + '0CellsStateOfCharge', + '0AverageCellTemperature', + '0Power', + 'propultionShaftSpeed', + 'systemUptime' +]); + +const state = { + logTimer: null, + logStreamer: null, + logsCount: 0, + isRecordingLogs: false, + currentLogFile: null, + currentLogKey: null, + openMeteoTimer: null, + hourlyArchiveTimer: null, + unsubPos: null, + app: null, + startTime: null +}; + +const logsDirectory = dataUtils.getDirectory(__dirname + '/datasetModels/saved_datas'); +const logsReferencesFile = path.join(__dirname, 'datasetModels/logs_references.json'); +const lastCallRef = { current: null }; + + +const getSKValue = (path, fallback = CONFIG.value_fallback) => { + if (!state.app) { + console.warn(`[getSKValue] App not initialized, returning fallback for path: ${path}`); + return fallback; + } + + try { + const value = state.app.getSelfPath(path)?.value; + return (value !== undefined && value !== null) ? value : fallback; + } catch (error) { + console.error(`[getSKValue] Error reading path ${path}:`, error.message); + return fallback; + } +}; + + +const closeStream = (stream) => { + return new Promise((resolve) => { + if (!stream || stream.destroyed) { + resolve(); + return; + } + + stream.end(() => { + resolve(); + }); + + setTimeout(resolve, 1000); + }); +}; + +const clearIntervalSafe = (timerId) => { + if (timerId) { + clearInterval(timerId); + } + return null; +}; + +const collectSensorData = (settings = {}) => { + // Prendi la posizione dalla navigazione se disponibile + const position = state.app?.getSelfPath('navigation.position')?.value; + const lat = position?.latitude ?? settings.latitude ?? CONFIG.number_value_fallback; + const lon = position?.longitude ?? settings.longitude ?? CONFIG.number_value_fallback; + + return { + timestamp: new Date().toISOString(), + wavesHeight: getSKValue("meb.waves.height"), + wavesPeriod: getSKValue("meb.waves.period"), + wavesDirection: getSKValue("meb.waves.direction"), + windSpeed: getSKValue("meb.wind.speed"), + windDirection: getSKValue("meb.wind.direction"), + temperature: getSKValue("meb.temperature"), + // currentSpeed: getSKValue("meb.currents.speed"), + // currentDirection: getSKValue("meb.currents.direction"), + speedOverGround: getSKValue("navigation.speedOverGround"), + courseOverGround: getSKValue("navigation.courseOverGroundTrue"), + headingTrue: getSKValue("navigation.headingTrue"), + latitude: lat, + longitude: lon, + '1Voltage': getSKValue("electrical.batteries.service.Voltage"), + '1Current': getSKValue("electrical.batteries.service.current"), + '1StateOfCharge': getSKValue("electrical.batteries.service.stateOfCharge"), + '1Temperature': getSKValue("electrical.batteries.service.temperature"), + '0Voltage': getSKValue("electrical.batteries.traction.Voltage"), + '0Current': getSKValue("electrical.batteries.traction.current"), + '0CellsStateOfCharge': getSKValue("electrical.batteries.traction.stateOfCharge"), + '0AverageCellTemperature': getSKValue("electrical.batteries.traction.temperature"), + '0Power': getSKValue("electrical.batteries.traction.power"), + propultionShaftSpeed: getSKValue("propulsion.0.revolutions"), + systemUptime: process.uptime() ?? CONFIG.number_value_fallback + }; +}; + + +function createNewFiles() { + try { + const now = new Date(); + const dateStr = now.toLocaleString('it-IT', { + timeZone: 'Europe/Rome', + day: 'numeric', + month: 'long', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + hour12: false + }).replace(/:/g, '-'); + const logFileName = `log_${dateStr}.csv`; + const logFile = path.join(logsDirectory, logFileName); + + // Close existing stream gracefully + if (state.logStreamer && !state.logStreamer.destroyed) { + state.logStreamer.end(); + } + + state.logStreamer = fs.createWriteStream(logFile, { flags: 'a' }); + + state.logStreamer.on('error', (err) => { + console.error('[log_file] Errore nello stream:', err); + }); + + dataset.datasetInit(CSV_HEADERS, state.logStreamer); + state.logsCount = 0; + + state.currentLogFile = logFileName; + state.currentLogKey = generateToken(); + + return true; + } catch (error) { + console.error('[log_file] Errore nella creazione di un nuovo file:', error); + return false; + } +} + +// ==================== RECORDING CONTROL ==================== + +/** + * Stops the data recording process + * @returns {boolean} True if stopped successfully, false if already stopped + */ +function stopRecording() { + if (!state.isRecordingLogs) { + return false; + } + + try { + state.logTimer = clearIntervalSafe(state.logTimer); + + if (state.logStreamer && !state.logStreamer.destroyed) { + state.logStreamer.end(); + } + + state.isRecordingLogs = false; + + // Usa la chiave generata all'inizio della sessione + if (state.currentLogFile && state.currentLogKey) { + const logFilePath = path.join(logsDirectory, state.currentLogFile); + + // Carica, aggiorna e salva references criptate + const logsData = loadSecureFile(logsReferencesFile, { references: [] }); + logsData.references.push({ + name: state.currentLogFile, + token: state.currentLogKey + }); + saveSecureFile(logsReferencesFile, logsData); + + // Cripta il file log con la stessa chiave + // encryptLog(logFilePath, state.currentLogKey); + + console.log(`[stopRecording] Log ${state.currentLogFile} criptato e salvato.`); + } + + state.logsCount = 0; + state.currentLogFile = null; + state.currentLogKey = null; + + return true; + } catch (error) { + console.error('[log_stop] Errore durante l\'arresto della registrazione:', error); + return false; + } +} + +/** + * Starts the data recording process + * @param {object} settings - Plugin settings + * @returns {boolean} True if started successfully, false if already running + */ +function startRecording(settings = {}) { + if (state.isRecordingLogs) { + return false; + } + + try { + state.isRecordingLogs = true; + state.startTime = Date.now(); + + if (!createNewFiles()) { + state.isRecordingLogs = false; + return false; + } + + state.logTimer = setInterval(() => { + try { + if (!state.logStreamer || state.logStreamer.destroyed) { + console.error('[log_dataset_error] Stream non disponibile'); + return; + } + const data = collectSensorData(settings); + const success = dataset.appendData(data, CSV_HEADERS, state.logStreamer); + if (success) { + state.logsCount++; + } + } catch (error) { + console.error('[log_dataset_error] Errore durante la raccolta dei dati:', error); + } + }, CONFIG.log_interval); + + return true; + } catch (error) { + console.error('[log_dataset_error] Errore nell\'avvio della registrazione', error); + state.isRecordingLogs = false; + return false; + } +} + +/** + * Restarts the recording process + * @param {object} settings - Plugin settings + * @returns {boolean} Success status + */ +function restartRecording(settings = {}) { + stopRecording(); + startRecording(settings); + return true; +} + +/** + * Gets current recording status with detailed metrics + * @returns {object} Status object + */ +function getRecordingStatus() { + return { + isRecording: state.isRecordingLogs, + recordCount: state.logsCount, + recordingInterval: CONFIG.log_interval, + uptime: state.startTime ? Date.now() - state.startTime : 0, + timestamp: new Date().toISOString() + }; +} + +module.exports = function (app) { + state.app = app; + + const plugin = { + id: "meb", + name: "MEB Plugin", + + start: async (settings) => { + try { + // ==================== WEB SOCKET AISSTREAM ==================== + try { + aisStream(); + } catch (error) { + console.error('[ERROR] Errore in AISStream:', error); + } + + // ==================== WEATHER UPDATES (OpenMeteo condiviso ogni 2 min) ==================== + + let location = { + latitude: app.getSelfPath('navigation.position')?.value?.latitude, + longitude: app.getSelfPath('navigation.position')?.value?.longitude, + }; + + const updateWeatherData = async () => { + const currentPos = app.getSelfPath('navigation.position')?.value; + if (currentPos?.latitude && currentPos?.longitude) { + location = { latitude: currentPos.latitude, longitude: currentPos.longitude }; + } else if (!location.latitude || !location.longitude) { + location = { + latitude: Number(settings?.latitude), + longitude: Number(settings?.longitude), + }; + } + + if (!location.latitude || !location.longitude) { + console.warn("[OpenMeteo] Posizione non disponibile"); + return; + } + + try { + const [forecastData, wavesData] = await Promise.all([ + getForecast(location), + getSeaConditions(location) + ]); + + // Log per debug + if (forecastData) { + console.log("[OpenMeteo] Forecast ricevuto:", { + temp: forecastData.temperature, + wind: forecastData.windSpeed, + humidity: forecastData.humidity + }); + } + + if (wavesData) { + console.log("[OpenMeteo] Marine ricevuto:", { + waveHeight: wavesData.waveHeight, + wavePeriod: wavesData.wavePeriod + }); + } + + // Aggiorna dati condivisi per grafici + graphsCore.updateSharedWeatherData(forecastData, wavesData); + + + // Pubblica su SignalK solo se abbiamo dati validi + const weatherPayload = { + temperature: forecastData?.temperature ?? null, + humidity: forecastData?.humidity ?? null, + pressure: forecastData?.pressure ?? null, + wind: { + speed: forecastData?.windSpeed ?? null, + direction: forecastData?.windDirection ?? null, + gusts: forecastData?.windGusts ?? null + }, + waves: { + height: wavesData?.waveHeight ?? null, + period: wavesData?.wavePeriod ?? null, + direction: wavesData?.waveDirection ?? null + }, + rain: forecastData?.rain ?? null, + precipitation: forecastData?.precipitation ?? null + }; + + publish(app, weatherPayload, settings); + console.log("[OpenMeteo] Dati pubblicati su SignalK"); + + } catch (error) { + console.error("[OpenMeteo] Errore aggiornamento:", error.message); + } + }; + + // Funzione per archiviare dati orari per grafici + const archiveHourlyData = () => { + const sharedData = graphsCore.getSharedWeatherData(); + if (sharedData.forecast || sharedData.waves) { + graphsCore.archiveHourlyData({ + temperature: sharedData.forecast?.temperature, + humidity: sharedData.forecast?.humidity, + pressure: sharedData.forecast?.pressure, + windSpeed: sharedData.forecast?.windSpeed, + windDirection: sharedData.forecast?.windDirection, + waveHeight: sharedData.waves?.waveHeight, + wavePeriod: sharedData.waves?.wavePeriod, + waveDirection: sharedData.waves?.waveDirection, + // currentSpeed: sharedData.waves?.currentVelocity, + // currentDirection: sharedData.waves?.currentDirection + }); + } + }; + + // Avvia aggiornamento meteo immediato + timer 2 minuti + updateWeatherData(); + state.openMeteoTimer = setInterval(updateWeatherData, CONFIG.openmeteo_interval); + + // Archivia dati ogni ora per i grafici + state.hourlyArchiveTimer = setInterval(archiveHourlyData, CONFIG.hourly_archive_interval); + + + // ==================== MAPPA INTERATTIVA ==================== + try { + mapHandler(app, settings); + } catch (error) { + console.error('[ERROR] Errore nell\'avvio della mappa:', error); + } + + // ==================== LOG DATI ==================== + try { + startRecording(settings); + } catch (error) { + console.error('[ERROR] Errore nell\'avvio dei log:', error); + } + + + app.datasetControl = { + start: () => startRecording(settings), + stop: stopRecording, + restart: () => restartRecording(settings), + getStatus: getRecordingStatus + }; + + // Esponi funzioni per modifica intervalli + app.intervalControl = { + updateInterval: (type, newIntervalMs) => { + const result = updateInterval(type, newIntervalMs); + if (!result) return null; + + // Riavvia il timer appropriato + if (result.type === 'openmeteo_interval') { + state.openMeteoTimer = clearIntervalSafe(state.openMeteoTimer); + updateWeatherData(); // Aggiorna subito + state.openMeteoTimer = setInterval(updateWeatherData, newIntervalMs); + console.log(`[IntervalControl] OpenMeteo interval aggiornato a ${newIntervalMs}ms`); + } else if (result.type === 'log_interval') { + // Riavvia recording con nuovo intervallo + const wasRecording = state.isRecordingLogs; + if (wasRecording) { + state.logTimer = clearIntervalSafe(state.logTimer); + state.logTimer = setInterval(() => { + try { + if (!state.logStreamer || state.logStreamer.destroyed) { + console.error('[log_dataset_error] Stream non disponibile'); + return; + } + const data = collectSensorData(settings); + const success = dataset.appendData(data, CSV_HEADERS, state.logStreamer); + if (success) { + state.logsCount++; + } + } catch (error) { + console.error('[log_dataset_error] Errore durante la raccolta dei dati:', error); + } + }, newIntervalMs); + } + console.log(`[IntervalControl] Log interval aggiornato a ${newIntervalMs}ms`); + } + + return result; + }, + getIntervals: () => ({ + log_interval: CONFIG.log_interval, + openmeteo_interval: CONFIG.openmeteo_interval, + hourly_archive_interval: CONFIG.hourly_archive_interval + }) + }; + + // ==================== BOT TELEGRAM (dopo intervalControl) ==================== + if (config.telegramBotToken) { + try { + await linkBot(app); + await send("✅ Computer di bordo attivo e pronto."); + console.log('[MEB TELEGRAM] Bot avviato con app.intervalControl disponibile'); + } catch (error) { + console.error('[ERROR] Errore nell\'avvio del bot telegram', error); + } + } else { + console.warn('[MEB TELEGRAM] Bot disabilitato: TELEGRAM_BOT_TOKEN non configurato.'); + } + + // ===== Shutdown Hooks ===== + const shutdown = async (reason = 'signal') => { + try { + console.log(`[shutdown] Received ${reason}. Stopping plugin...`); + await plugin.stop(); + process.exit(0); + } catch (err) { + console.error('[shutdown] Error during stop:', err); + process.exit(1); + } + }; + + // Evita di registrare multipli handler + if (!process.__meb_shutdown_hooks_installed) { + process.__meb_shutdown_hooks_installed = true; + process.on('SIGINT', () => shutdown('SIGINT')); + process.on('SIGTERM', () => shutdown('SIGTERM')); + process.on('uncaughtException', (err) => { + console.error('[uncaughtException]', err); + shutdown('uncaughtException'); + }); + process.on('unhandledRejection', (reason) => { + console.error('[unhandledRejection]', reason); + shutdown('unhandledRejection'); + }); + } + + } catch (error) { + console.error('[Errore] Errore durante l\'avvio del plugin:', error); + throw error; + } + }, + + stop: async () => { + try { + state.openMeteoTimer = clearIntervalSafe(state.openMeteoTimer); + state.hourlyArchiveTimer = clearIntervalSafe(state.hourlyArchiveTimer); + + if (typeof state.unsubPos === "function") { + try { + state.unsubPos(); + state.unsubPos = null; + } catch (error) { + console.error('[ERROR] Errore durante la cancellazione dell\'iscrizione alla posizione:', error); + } + } + + // stopRecording gestisce già criptazione e salvataggio reference + if (app.datasetControl) { + try { + app.datasetControl.stop(); + } catch (error) { + console.error('[ERROR] Errore durante l\'arresto del controllo del dataset:', error); + } + } + + await closeStream(state.logStreamer); + console.log('[stop] Plugin arrestato correttamente.'); + + } catch (error) { + console.error('[ERROR] Errore durante l\'arresto del plugin:', error); + } + }, + + schema: () => ({ + type: "object", + required: [], + properties: {}, + }), + + registerWithRouter: (router) => { + setupRoutes(router, lastCallRef, app); + }, + + getOpenApi: getOpenApiSpec, + }; + + return plugin; +}; \ No newline at end of file diff --git a/plugin/public/css/data_console.css b/plugin/public/css/data_console.css new file mode 100644 index 0000000..31beb7b --- /dev/null +++ b/plugin/public/css/data_console.css @@ -0,0 +1,58 @@ +.data-console-container { + font-family: sans-serif; + background-color: #f4f4f4; + padding: 15px; + border-radius: 25px; + display: flex; + align-items: center; +} + +#error-popup { + position: fixed; + z-index: 9999; + inset: 0; + background: rgba(0,0,0,0.4); + display: none; + justify-content: center; + align-items: center; +} + +#error-popup-content { + background: #fff; + padding: 20px 25px; + border-radius: 8px; + max-width: 400px; + width: 90%; + box-shadow: 0 2px 10px rgba(0,0,0,0.2); + position: relative; + font-family: sans-serif; +} + +#error-popup-close { + position: absolute; + right: 10px; + top: 5px; + cursor: pointer; + font-size: 20px; +} + +table { + width: 100%; + border-collapse: collapse; + margin-top: 30px; + border: 0px solid #f9f9f9; + font-family: sans-serif; +} + +table thead { + background-color: #e5effa; + font-size: 13px; + color: rgb(0, 0, 0); + +} + +table.th, table td { + padding: 10px 10px; + text-align: left; + border-bottom: 1px solid #ddd; +} \ No newline at end of file diff --git a/plugin/public/css/helm_suggestions.css b/plugin/public/css/helm_suggestions.css new file mode 100644 index 0000000..92a68a9 --- /dev/null +++ b/plugin/public/css/helm_suggestions.css @@ -0,0 +1,177 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + background-color: #f4f4f4; + color: #333; + margin: 0; + padding: 20px; +} + +.widget-container { + width: 100%; + max-width: 600px; + padding: 30px; + background: #ffffff; + border-radius: 10px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + text-align: center; +} + +.visualization-container { + position: relative; + width: 100%; + height: 300px; + margin: 20px auto; + display: flex; + justify-content: center; + align-items: center; + background: #fafafa; + border-radius: 8px; + border: 1px solid #eee; +} + +.arrow-svg { + width: 300px; + height: 300px; + overflow: visible; +} + +.arrow-head { + fill: none; + stroke: #007aff; + stroke-width: 12; + stroke-linecap: round; + stroke-linejoin: round; + filter: drop-shadow(0 2px 4px rgba(0, 122, 255, 0.3)); + transition: transform 0.1s linear; +} + +.arrow-stem { + fill: none; + stroke: #007aff; + stroke-width: 12; + stroke-linecap: round; + transition: d 0.1s linear; + filter: drop-shadow(0 2px 4px rgba(0, 122, 255, 0.3)); + transform: rotate(-40deg); +} + +.controls-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; + margin: 30px 0 20px; + text-align: left; +} + +.control-group { + background: #f8f9fa; + padding: 15px; + border-radius: 8px; +} + +.control-group label { + display: block; + font-size: 12px; + font-weight: 600; + color: #666; + margin-bottom: 8px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.control-group input[type="number"] { + width: 100%; + padding: 10px; + border: 2px solid #e0e0e0; + border-radius: 6px; + font-size: 16px; + font-weight: 600; + color: #333; + transition: border-color 0.3s ease; + box-sizing: border-box; +} + +.control-group input[type="number"]:focus { + outline: none; + border-color: #007aff; +} + +.slider-container { + width: 100%; + margin-top: 30px; + position: relative; + padding: 0 10px; + box-sizing: border-box; +} + +input[type=range] { + width: 100%; + cursor: pointer; + height: 8px; + border-radius: 5px; + outline: none; + -webkit-appearance: none; + appearance: none; + background: #e0e0e0; + transition: background 0.3s ease; +} + +input[type=range]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 24px; + height: 24px; + border-radius: 50%; + background: #007aff; + cursor: pointer; + box-shadow: 0 2px 8px rgba(0, 122, 255, 0.4); + transition: transform 0.2s ease; + margin-top: -8px; +} + +input[type=range]::-webkit-slider-thumb:hover { + transform: scale(1.1); +} + +.stats { + display: flex; + justify-content: space-around; + margin-top: 30px; + padding-top: 20px; + border-top: 1px solid #e0e0e0; +} + +.stat-item { + text-align: center; +} + +.stat-value { + font-size: 24px; + font-weight: bold; + color: #007aff; + transition: all 0.3s ease; +} + +.stat-label { + font-size: 12px; + color: #888; + margin-top: 5px; + text-transform: uppercase; +} + +.percentage-display { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 40px; + font-weight: 800; + color: rgba(0, 0, 0, 0.1); + pointer-events: none; + z-index: 0; +} \ No newline at end of file diff --git a/plugin/public/decrypt_tool.html b/plugin/public/decrypt_tool.html new file mode 100644 index 0000000..0948500 --- /dev/null +++ b/plugin/public/decrypt_tool.html @@ -0,0 +1,785 @@ + + + + + + MEB - Decryption Tool + + + +
+
+

🔐 MEB Decryption Tool

+

Decripta i file CSV criptati con AES-256-GCM

+
+ + +
+

📁 Importa File Criptato

+ +
+ ⬆️ +

Trascina qui il file criptato o clicca per selezionarlo

+ Formati supportati: .csv (criptati) +
+ + +
+
+
+
+
+ + +
+

🔑 Chiave di Decriptazione

+ +
+
+ + +
+
+ +
+ ℹ️ Formato chiave supportato:
+ • Token esadecimale (48 caratteri): 217af80a15d54289...
+ • Qualsiasi stringa (verrà hashata con SHA-256)
+ • Algoritmo: AES-256-GCM con IV (12 byte) + Auth Tag (16 byte) +
+
+ + +
+ + +
+ + +
+
+

📄 Anteprima Contenuto

+ +
+
+ +
+ + + +
+
+
+
+ + + + diff --git a/plugin/public/graphs.html b/plugin/public/graphs.html new file mode 100644 index 0000000..f03d00d --- /dev/null +++ b/plugin/public/graphs.html @@ -0,0 +1,386 @@ + + + + + + Previsioni - 7 giorni + + + + +
+

📊 MEB Grafici Meteo

+
+
+ Aggiornamento... +
+
+ +
+ + + + +
+ +
+
+
🌡️ Temperatura Attuale
+
--
+
°C
+
+
+
🌬️ Vento
+
--
+
km/h
+
+
+
🌊 Altezza Onde
+
--
+
m
+
+
+
💧 Umidità
+
--
+
%
+
+
+ +
+
+

🌡️ Temperatura

+
+ +
+
+ +
+

🌬️ Velocità Vento

+
+ +
+
+ +
+

🌊 Altezza Onde

+
+ +
+
+ +
+

💧 Umidità

+
+ +
+
+
+ + + + diff --git a/plugin/public/steering_support/helm_steering_destra.html b/plugin/public/steering_support/helm_steering_destra.html new file mode 100644 index 0000000..b3d3f74 --- /dev/null +++ b/plugin/public/steering_support/helm_steering_destra.html @@ -0,0 +1,336 @@ + + + + + + + Helm Steering UI + + + + + + + +
+

Helm Steering Control - Destra

+ +
+
0%
+ + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+ + +
+
+ +
+ +
+ +
+
+
0
+
Valore Attuale
+
+
+
0%
+
Progresso
+
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/plugin/public/steering_support/helm_steering_sinistra.html b/plugin/public/steering_support/helm_steering_sinistra.html new file mode 100644 index 0000000..d16707f --- /dev/null +++ b/plugin/public/steering_support/helm_steering_sinistra.html @@ -0,0 +1,158 @@ + + + + + + + Helm Steering UI + + + + + + + +
+

Helm Steering Control - Sinistra

+ +
+
0%
+ + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+ + +
+
+ +
+ +
+ +
+
+
0
+
Valore Attuale
+
+
+
0%
+
Progresso
+
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/plugin/public/steering_support/steering_helm_tip_builder.html b/plugin/public/steering_support/steering_helm_tip_builder.html new file mode 100644 index 0000000..732785a --- /dev/null +++ b/plugin/public/steering_support/steering_helm_tip_builder.html @@ -0,0 +1,588 @@ + + + + + + + Steering Suggestions Widget + + + + + + +
+

Progress Circle

+ +
+ + + + + + + + + + + +
0%
+ +
+ + + +
+
+ + +
Punto di partenza
+
+ +
+ + +
Arco massimo
+
+
+ + +
+ Direzione +
+ + Oraria +
+ +
+ +
+ +
+ 0% + 50% + 100% +
+
+ +
+
+
+
Progresso
+
+
+
+
Arco attuale
+
+
+
100%
+
Rimanente
+
+
+ +
+

Controllo Trasformazioni Freccia

+
+
+ + +
70°
+
+ +
+ + +
0
+
+ +
+ + +
0
+
+ +
+ + +
1x
+
+ +
+ + +
80
+
+ +
+ + +
-150
+
+ +
+ + +
+
+
+ +
+ + + + + + \ No newline at end of file diff --git a/plugin/sensors.references.json b/plugin/sensors.references.json new file mode 100644 index 0000000..6690683 --- /dev/null +++ b/plugin/sensors.references.json @@ -0,0 +1,68 @@ +{ + "production": [ + { + "collection": "temperature", + "main_path": "meb.temperature", + "elements": null + }, + { + "collection": "wind", + "main_path": "meb.wind", + "elements": [ + {"direction": "direction"}, + {"speed": "speed"} + ] + }, + { + "collection": "waves", + "main_path": "meb.waves", + "elements": [ + {"direction": "direction"}, + {"height": "height"}, + {"period": "period"} + ] + }, + { + "collection": "position", + "main_path": "navigation", + "elements": [ + {"latitude": "position.latitude"}, + {"longitude": "position.longitude"}, + {"headingTrue": "headingTrue"}, + {"speedOverGround": "speedOverGround"}, + {"courseOverGround": "courseOverGroundTrue"} + ] + }, + { + "collection": "service_battery", + "main_path": "electrical.batteries.service", + "elements": [ + {"voltage": "Voltage"}, + {"current": "current"}, + {"stateOfCharge": "stateOfCharge"} + ] + }, + { + "collection": "traction_battery", + "main_path": "electrical.batteries.traction", + "elements": [ + {"voltage": "Voltage"}, + {"current": "current"}, + {"stateOfCharge": "stateOfCharge"}, + {"temperature": "temperature"}, + {"power": "power"} + ] + }, + { + "collection": "engine", + "main_path": "propulsion.0", + "elements": [ + {"proipultionShaftSpeed": "revolutions"} + ] + }, + { + "collection": "system", + "main_path": "system.uptime" + } + ] +} \ No newline at end of file diff --git a/plugin/tools/crypt.js b/plugin/tools/crypt.js new file mode 100644 index 0000000..c817a7c --- /dev/null +++ b/plugin/tools/crypt.js @@ -0,0 +1,258 @@ +/** + * Modulo di crittografia centralizzato per MEB Plugin + * Supporta AES-256-GCM per file sensibili e log CSV + * + * BEST PRACTICES SICUREZZA ENTERPRISE: + * 1. La MASTER_KEY dovrebbe essere in variabile d'ambiente (process.env.MEB_MASTER_KEY) + * 2. In produzione usare AWS KMS, HashiCorp Vault, o Azure Key Vault + * 3. Rotazione periodica delle chiavi (ogni 90 giorni) + * 4. Separazione chiavi: una per users, una per logs_references, una per log files + * 5. Audit log di ogni accesso ai file sensibili + * + * GENERAZIONE CHIAVE SICURA: + * Esegui nel terminale: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" + * Poi imposta: export MEB_MASTER_KEY="la_chiave_generata" + */ + +const crypto = require("crypto"); +const fs = require('fs'); + + +const MASTER_KEY_HEX = process.env.CRYPTOKEY || null; +const TOKEN_CHARSET = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789'; +const specialCharset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*_+-='; + + +/** + * Ottiene la chiave master (32 byte per AES-256) + * @returns {Buffer} Chiave di 32 byte + */ +function getMasterKey() { + if (!MASTER_KEY_HEX) { + throw new Error("MASTER_KEY non definita. Imposta MEB_MASTER_KEY nelle variabili d'ambiente."); + } + const key = Buffer.from(MASTER_KEY_HEX, 'hex'); + if (key.length !== 32) { + throw new Error("MASTER_KEY deve essere di 32 byte (64 caratteri hex)."); + } + return key; +} + +/** + * Normalizza qualsiasi chiave custom a 32 byte Buffer per AES-256. + * Accetta chiavi di qualsiasi lunghezza/formato. + * @param {string|Buffer|null} customKey - Chiave custom o null per usare master key + * @returns {Buffer} Chiave di 32 byte + */ +function normalizeKey(customKey) { + if (!customKey) return getMasterKey(); + + if (typeof customKey === 'string') { + // Se è hex di 64 caratteri, convertilo direttamente + if (/^[0-9a-fA-F]{64}$/.test(customKey)) { + return Buffer.from(customKey, 'hex'); + } + // Altrimenti hash SHA-256 per ottenere 32 byte + return crypto.createHash('sha256').update(customKey, 'utf8').digest(); + } + + if (Buffer.isBuffer(customKey)) { + if (customKey.length === 32) return customKey; + return crypto.createHash('sha256').update(customKey).digest(); + } + + throw new Error("customKey deve essere una stringa o un Buffer"); +} + +// ==================== GENERAZIONE TOKEN ==================== + +/** + * Genera un token esadecimale casuale UNICO ogni volta + * @param {number} bytes - Numero di byte (default 24 = 48 caratteri hex) + * @returns {string} Token esadecimale unico + */ +function generateToken(bytes = 24) { + return crypto.randomBytes(bytes).toString('hex'); +} + +/** + * Genera un token leggibile (senza caratteri ambigui come 0/O, 1/l/I) + * Più facile da comunicare verbalmente + * @param {number} length - Lunghezza del token (default 32) + * @returns {string} Token alfanumerico leggibile + */ +function generateReadableToken(length = 32) { + const bytes = crypto.randomBytes(length); + let result = ''; + for (let i = 0; i < length; i++) { + result += TOKEN_CHARSET[bytes[i] % TOKEN_CHARSET.length]; + } + return result; +} + +/** + * Genera un token con caratteri speciali (più sicuro per chiavi sensibili) + * @param {number} length - Lunghezza del token (default 64) + * @returns {string} Token con caratteri speciali + */ +function generateSecureToken(length = 64) { + + const bytes = crypto.randomBytes(length); + let result = ''; + for (let i = 0; i < length; i++) { + result += specialCharset[bytes[i] % specialCharset.length]; + } + return result; +} + +// ==================== CRITTOGRAFIA OGGETTI JSON (per file sensibili) ==================== + +/** + * Cripta un oggetto JSON in Buffer binario (AES-256-GCM) + * Usato per telegram_users.json e logs_references.json + * @param {object} obj - Oggetto da criptare + * @param {string|Buffer|null} customKey - Chiave custom (opzionale) + * @returns {Buffer} Dati criptati [IV(12) + TAG(16) + CIPHERTEXT] + */ +// DISABILITATO: salviamo in chiaro +function encrypt(obj, customKey = null) { + const plaintext = Buffer.from(JSON.stringify(obj), 'utf8'); + return plaintext; // ritorna direttamente il contenuto in chiaro +} + +/** + * Decripta un Buffer in oggetto JSON + * @param {Buffer} buffer - Dati criptati + * @param {string|Buffer|null} customKey - Chiave custom (opzionale) + * @returns {object} Oggetto decriptato (array vuoto se fallisce) + */ +// DISABILITATO: leggiamo direttamente in chiaro +function decrypt(buffer, customKey = null) { + try { + if (!buffer) return []; + const content = buffer.toString('utf8'); + return JSON.parse(content); + } catch (error) { + console.error('[decrypt] Errore:', error.message); + return []; + } +} + +// ==================== CRITTOGRAFIA FILE LOG CSV ==================== + +/** + * Cripta un file CSV/testo sul disco + * @param {string} filePath - Percorso del file + * @param {string|Buffer|null} customKey - Chiave custom (qualsiasi lunghezza) + * @returns {boolean} True se successo + */ +// DISABILITATO: i file log rimangono sempre in chiaro +function encryptLog(filePath, customKey = null) { + try { + // Non fare nulla, lascia il file in chiaro + return true; + } catch (error) { + console.error('[encryptLog] Errore:', error.message); + return false; + } +} + +/** + * Decripta un file CSV/testo e lo riscrive sul disco + * @param {string} filePath - Percorso del file criptato + * @param {string|Buffer|null} customKey - Chiave custom + * @returns {string|null} Contenuto decriptato o null se errore + */ +// DISABILITATO: i file sono già in chiaro +function decryptLog(filePath, customKey = null) { + try { + const content = fs.readFileSync(filePath, 'utf8'); + return content; // ritorna contenuto in chiaro senza modifiche + } catch (error) { + console.error('[decryptLog] Errore:', error.message); + return null; + } +} + +/** + * Decripta un file log e restituisce il contenuto SENZA modificare il file + * @param {string} filePath - Percorso del file criptato + * @param {string|Buffer|null} customKey - Chiave custom + * @returns {string|null} Contenuto decriptato o null se errore + */ +// DISABILITATO: i file sono già in chiaro +function decryptLogToMemory(filePath, customKey = null) { + try { + return fs.readFileSync(filePath, 'utf8'); + } catch (error) { + console.error('[decryptLogToMemory] Errore:', error.message); + return null; + } +} + +// ==================== GESTIONE FILE SENSIBILI (telegram_users, logs_references) ==================== + +/** + * Carica e decripta un file JSON sensibile + * Gestisce automaticamente file in chiaro (migrazione) e criptati + * @param {string} filePath - Percorso del file + * @param {object} defaultValue - Valore di default se file non esiste + * @returns {object} Dati decriptati + */ +function loadSecureFile(filePath, defaultValue = {}) { + try { + if (!fs.existsSync(filePath)) { + return defaultValue; + } + const content = fs.readFileSync(filePath, 'utf8').trim(); + try { + return JSON.parse(content); + } catch (e) { + console.error(`[loadSecureFile] JSON non valido in ${filePath}:`, e.message); + return defaultValue; + } + } catch (error) { + console.error(`[loadSecureFile] Errore caricamento ${filePath}:`, error.message); + return defaultValue; + } +} + +/** + * Cripta e salva un file JSON sensibile + * @param {string} filePath - Percorso del file + * @param {object} data - Dati da salvare + * @returns {boolean} True se successo + */ +function saveSecureFile(filePath, data) { + try { + const content = JSON.stringify(data, null, 2); + fs.writeFileSync(filePath, content, 'utf8'); + return true; + } catch (error) { + console.error(`[saveSecureFile] Errore salvataggio ${filePath}:`, error.message); + return false; + } +} + +module.exports = { + // Generazione token + generateToken, + generateReadableToken, + generateSecureToken, + + // Crittografia oggetti JSON + encrypt, + decrypt, + + // Crittografia file log + encryptLog, + decryptLog, + decryptLogToMemory, + + // Gestione file sensibili + loadSecureFile, + saveSecureFile, + + // Utility + normalizeKey +}; \ No newline at end of file diff --git a/plugin/tools/logRecorder.js b/plugin/tools/logRecorder.js new file mode 100644 index 0000000..2fa60c7 --- /dev/null +++ b/plugin/tools/logRecorder.js @@ -0,0 +1,219 @@ +/** + * logRecorder.js - Gestione registrazione dati separata + * Centralizza tutte le funzioni di logging del dataset + */ + +const path = require('path'); +const { datasetInit, appendData } = require('../datasetModels/datasetCore'); + +let app = null; +let recordingInterval = null; +let isRecording = false; + +// Stato condiviso della registrazione +const recordingState = { + active: false, + startTime: null, + entryCount: 0, + currentFile: null, + stream: null +}; + +/** + * Inizializza il recorder con l'istanza di SignalK app + */ +function init(signalkApp) { + app = signalkApp; + console.log('[LogRecorder] Inizializzato'); +} + +/** + * Raccoglie i dati dai sensori SignalK + */ +function collectSensorData() { + const getSK = (p) => { + const v = app.getSelfPath(p); + return v && v.value !== undefined && v.value !== null ? v.value : null; + }; + + return { + timestamp: new Date().toISOString(), + + // Posizione + latitude: getSK('navigation.position')?.latitude ?? null, + longitude: getSK('navigation.position')?.longitude ?? null, + speed: getSK('navigation.speedOverGround'), + heading: getSK('navigation.headingTrue'), + + // Batteria Trazione + traction_voltage: getSK('electrical.batteries.traction.Voltage'), + traction_current: getSK('electrical.batteries.traction.current'), + traction_soc: getSK('electrical.batteries.traction.stateOfCharge'), + traction_temperature: getSK('electrical.batteries.traction.temperature'), + traction_power: getSK('electrical.batteries.traction.power'), + + // Batteria Servizio + service_voltage: getSK('electrical.batteries.service.Voltage'), + service_current: getSK('electrical.batteries.service.current'), + service_soc: getSK('electrical.batteries.service.stateOfCharge'), + service_temperature: getSK('electrical.batteries.service.temperature'), + + // Meteo (da OpenMeteo condiviso) + temperature: getSK('meb.temperature'), + windSpeed: getSK('meb.appleWindSpeed'), + windDirection: getSK('meb.appleWindDirection'), + + // Onde + waveHeight: getSK('meb.waves.waveHeight'), + wavePeriod: getSK('meb.waves.wavePeriod'), + waveDirection: getSK('meb.waves.waveDirection') + }; +} + +/** + * Crea un nuovo file di log + */ +function createNewLogFile() { + const headers = [ + 'timestamp', + 'latitude', 'longitude', 'speed', 'heading', + 'traction_voltage', 'traction_current', 'traction_soc', 'traction_temperature', 'traction_power', + 'service_voltage', 'service_current', 'service_soc', 'service_temperature', + 'temperature', 'windSpeed', 'windDirection', + 'waveHeight', 'wavePeriod', 'waveDirection' + ]; + + const result = datasetInit(headers); + if (result) { + recordingState.currentFile = result.fileName; + recordingState.stream = result.stream; + console.log(`[LogRecorder] Nuovo file: ${result.fileName}`); + } + return result; +} + +/** + * Scrive una riga di dati nel log + */ +function writeLogEntry(data) { + if (!recordingState.stream) return false; + + const values = [ + data.timestamp, + data.latitude, data.longitude, data.speed, data.heading, + data.traction_voltage, data.traction_current, data.traction_soc, data.traction_temperature, data.traction_power, + data.service_voltage, data.service_current, data.service_soc, data.service_temperature, + data.temperature, data.windSpeed, data.windDirection, + data.waveHeight, data.wavePeriod, data.waveDirection + ]; + + appendData(values); + recordingState.entryCount++; + return true; +} + +/** + * Avvia la registrazione + * @param {number} intervalMs - Intervallo in millisecondi (default 2000) + */ +function startRecording(intervalMs = 2000) { + if (isRecording) { + console.log('[LogRecorder] Registrazione già attiva'); + return false; + } + + if (!app) { + console.error('[LogRecorder] App non inizializzata'); + return false; + } + + const fileResult = createNewLogFile(); + if (!fileResult) { + console.error('[LogRecorder] Impossibile creare file di log'); + return false; + } + + recordingState.active = true; + recordingState.startTime = Date.now(); + recordingState.entryCount = 0; + isRecording = true; + + recordingInterval = setInterval(() => { + const data = collectSensorData(); + writeLogEntry(data); + }, intervalMs); + + console.log(`[LogRecorder] Registrazione avviata (ogni ${intervalMs}ms)`); + return true; +} + +/** + * Ferma la registrazione + */ +function stopRecording() { + if (!isRecording) { + console.log('[LogRecorder] Nessuna registrazione attiva'); + return false; + } + + if (recordingInterval) { + clearInterval(recordingInterval); + recordingInterval = null; + } + + if (recordingState.stream) { + recordingState.stream.end(); + } + + const duration = Date.now() - recordingState.startTime; + console.log(`[LogRecorder] Registrazione fermata. Durata: ${Math.round(duration / 1000)}s, Entries: ${recordingState.entryCount}`); + + recordingState.active = false; + recordingState.stream = null; + isRecording = false; + + return { + duration, + entries: recordingState.entryCount, + file: recordingState.currentFile + }; +} + +/** + * Riavvia la registrazione (nuovo file) + */ +function restartRecording(intervalMs = 2000) { + stopRecording(); + return startRecording(intervalMs); +} + +/** + * Ottiene lo stato corrente della registrazione + */ +function getStatus() { + return { + isRecording, + active: recordingState.active, + startTime: recordingState.startTime, + entryCount: recordingState.entryCount, + currentFile: recordingState.currentFile, + runningTime: isRecording ? Date.now() - recordingState.startTime : 0 + }; +} + +/** + * Verifica se la registrazione è attiva + */ +function isActive() { + return isRecording; +} + +module.exports = { + init, + startRecording, + stopRecording, + restartRecording, + getStatus, + isActive, + collectSensorData +}; diff --git a/plugin/tools/map.handler.js b/plugin/tools/map.handler.js new file mode 100644 index 0000000..fc86339 --- /dev/null +++ b/plugin/tools/map.handler.js @@ -0,0 +1,35 @@ +const fs = require("fs"); +const path = require("path"); + +module.exports = function(app, settings) { + // Serve mappa + app.get('/meb/map', (req, res) => { + const filePath = path.join(__dirname, "public", "map.html"); + fs.readFile(filePath, "utf8", (err, html) => { + if (err) { + res.status(500).send("Errore nel caricamento della mappa"); + return; + } + const token = settings?.mapboxKey ?? ""; + const finalHtml = html.replace("{{MAPBOX_KEY}}", token); + res.setHeader("Content-Type", "text/html; charset=utf-8"); + res.send(finalHtml); + }); + }); + + // WebSocket forward: posizione in tempo reale + let lastPosition = null; + + app.streambundle.getSelfStream("navigation.position").onValue(pos => { + lastPosition = pos; + }); + + // Endpoint JSON per marker barca (se vuoi usarlo invece del WS SignalK) + app.get('/meb/map/boat', (req, res) => { + if (!lastPosition) { + res.json({ error: "No position data available" }); + return; + } + res.json(lastPosition); + }); +} diff --git a/plugin/tools/public/map.html b/plugin/tools/public/map.html new file mode 100644 index 0000000..17427d1 --- /dev/null +++ b/plugin/tools/public/map.html @@ -0,0 +1,262 @@ + + + + +Mappa Meteo SignalK + + + + + + + + + +
+
+ Caricamento dati... +
+ + + + + \ No newline at end of file diff --git a/plugin/tools/publisher.js b/plugin/tools/publisher.js new file mode 100644 index 0000000..d4f6718 --- /dev/null +++ b/plugin/tools/publisher.js @@ -0,0 +1,74 @@ +/** + * publisher.js - Pubblica dati su SignalK + */ + +/** + * Genera valori SignalK da un oggetto dati + * @param {Object} data - Dati da convertire + * @param {string} prefix - Prefisso per i path SignalK + * @returns {Array} Array di valori SignalK + */ +function generateValues(data, prefix = "meb") { + if (!data || typeof data !== 'object') { + return []; + } + + const values = []; + + function traverse(obj, pathParts) { + for (const key in obj) { + if (!Object.prototype.hasOwnProperty.call(obj, key)) continue; + + const val = obj[key]; + if (val === undefined || val === null) continue; + + const newPath = [...pathParts, key]; + + if (typeof val === "object" && !Array.isArray(val)) { + traverse(val, newPath); + } else if (!Array.isArray(val)) { + // Ignora array, pubblica solo valori primitivi + values.push({ + path: newPath.join("."), + value: val, + meta: { displayName: key }, + }); + } + } + } + + traverse(data, [prefix]); + return values; +} + +/** + * Pubblica dati meteo su SignalK + * @param {Object} app - Istanza app SignalK + * @param {Object} weatherData - Dati meteo da pubblicare + * @param {Object} settings - Impostazioni plugin + */ +function publishWeatherData(app, weatherData, settings) { + if (!app || !weatherData) { + console.warn('[Publisher] App o dati non disponibili'); + return; + } + + const values = generateValues(weatherData); + + if (values.length === 0) { + console.debug('[Publisher] Nessun valore da pubblicare'); + return; + } + + console.debug(`📤 Pubblicazione ${values.length} valori SignalK`); + + try { + app.handleMessage("meb", { + updates: [{ values }], + }); + } catch (error) { + console.error('[Publisher] Errore pubblicazione:', error.message); + } +} + +module.exports = { publish: publishWeatherData }; \ No newline at end of file diff --git a/plugin/tools/routes.js b/plugin/tools/routes.js new file mode 100644 index 0000000..41f63c0 --- /dev/null +++ b/plugin/tools/routes.js @@ -0,0 +1,321 @@ +function setupRoutes(router, lastCallRef, app) { + router.get("/ping", async (req, res) => { + try { + const text = lastCallRef.current || "pong"; + res.status(200).sendFile(__dirname + "/steering_support/helm_steering_destra.html"); + } catch (e) { + res.status(500).json({ error: e.message }); + } + }); + + router.get("/helm_steering_destro", (req, res) => { + try { + res.status(200).sendFile(__dirname + "/steering_support/helm_steering_destro.html"); + } catch (e) { + res.status(500).json({ error: e.message }); + } + }); + + router.get("/tools", (req, res) => { + try { + const path = require("path"); + const filePath = path.join(__dirname, "..", "public", "decrypt_tool.html"); + res.status(200).sendFile(filePath); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + // LOGS DATASETS + router.post("/dataset/start", (req, res) => { + try { + if (!app.datasetControl) { + return res.status(503).json({ error: "Dataset control non disponibile" }); + } + const result = app.datasetControl.start(); + res.json({ success: result, message: result ? "Registrazione avviata" : "Registrazione già in corso" }); + } catch (e) { + res.status(500).json({ error: e.message }); + } + }); + + router.post("/dataset/stop", (req, res) => { + try { + if (!app.datasetControl) { + return res.status(503).json({ error: "Dataset control non disponibile" }); + } + const result = app.datasetControl.stop(); + res.json({ success: result, message: result ? "Registrazione fermata" : "Nessuna registrazione in corso" }); + } catch (e) { + res.status(500).json({ error: e.message }); + } + }); + + router.post("/dataset/restart", (req, res) => { + try { + if (!app.datasetControl) { + return res.status(503).json({ error: "Dataset control non disponibile" }); + } + const result = app.datasetControl.restart(); + res.json({ success: result, message: "Registrazione riavviata" }); + } catch (e) { + res.status(500).json({ error: e.message }); + } + }); + + router.get("/dataset/status", (req, res) => { + try { + if (!app.datasetControl) { + return res.status(503).json({ error: "Dataset control non disponibile" }); + } + const status = app.datasetControl.getStatus(); + res.json(status); + } catch (e) { + res.status(500).json({ error: e.message }); + } + }); + + router.get("/dataset/files", (req, res) => { + try { + const fs = require('fs'); + const path = require('path'); + const logsDirectory = path.join(__dirname, '..', 'datasetModels', 'saved_datas'); + + if (!fs.existsSync(logsDirectory)) { + return res.json({ files: [], count: 0 }); + } + + const items = fs.readdirSync(logsDirectory); + const files = items + .filter(item => { + const fullPath = path.join(logsDirectory, item); + return fs.statSync(fullPath).isFile(); + }) + .map(file => { + const fullPath = path.join(logsDirectory, file); + const stats = fs.statSync(fullPath); + return { + name: file, + size: stats.size, + created: stats.birthtime, + modified: stats.mtime + }; + }) + .sort((a, b) => b.modified.getTime() - a.modified.getTime()); + + res.json({ files, count: files.length }); + } catch (e) { + res.status(500).json({ error: e.message }); + } + }); + + // ==================== GRAPHS API ==================== + const graphsCore = require('../datasetModels/graphsCore.js'); + + // Serve la pagina HTML dei grafici + router.get("/graphs", (req, res) => { + try { + const path = require("path"); + const filePath = path.join(__dirname, "..", "public", "graphs.html"); + res.status(200).sendFile(filePath); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + // API per ottenere dati grafici + router.get("/api/graphs", (req, res) => { + try { + const hours = parseInt(req.query.hours) || 24; + const data = graphsCore.getAllGraphsData(hours); + + // Aggiungi valori attuali dalla cache condivisa + const sharedData = graphsCore.getSharedWeatherData(); + data.current = { + temperature: sharedData.forecast?.temperature, + windSpeed: sharedData.forecast?.windSpeed, + waveHeight: sharedData.waves?.waveHeight, + humidity: sharedData.forecast?.humidity + }; + + // Aggiungi unità di misura + data.units = graphsCore.getUnits(); + + res.json(data); + } catch (e) { + res.status(500).json({ error: e.message }); + } + }); + + // API per statistiche archivio + router.get("/api/graphs/stats", (req, res) => { + try { + const stats = graphsCore.getArchiveStats(); + res.json(stats); + } catch (e) { + res.status(500).json({ error: e.message }); + } + }); + +} + +function getOpenApiSpec() { + return { + openapi: "3.0.0", + info: { title: "MebWeather API Portal", version: "1.0.0" }, + servers: [{ url: "/plugins/meb-weather" }], + paths: { + "/ping": { + get: { + summary: "Called /ping route", + responses: { + 200: { + description: "OK", + content: { + "application/json": { + schema: { + type: "object", + properties: { message: { type: "string" } }, + }, + }, + }, + }, + }, + }, + }, + "/meb/suggestion": { + get: { + summary: "Pagina di test MEB Suggestion", + responses: { + 200: { + description: "OK", + content: { + "text/html": { + schema: { type: "string" }, + }, + }, + }, + }, + }, + }, + "/dataset/start": { + post: { + summary: "Avvia la registrazione dataset", + responses: { + 200: { + description: "Registrazione avviata", + content: { + "application/json": { + schema: { + type: "object", + properties: { + success: { type: "boolean" }, + message: { type: "string" } + }, + }, + }, + }, + }, + }, + }, + }, + "/dataset/stop": { + post: { + summary: "Ferma la registrazione dataset", + responses: { + 200: { + description: "Registrazione fermata", + content: { + "application/json": { + schema: { + type: "object", + properties: { + success: { type: "boolean" }, + message: { type: "string" } + }, + }, + }, + }, + }, + }, + }, + }, + "/dataset/restart": { + post: { + summary: "Riavvia la registrazione dataset", + responses: { + 200: { + description: "Registrazione riavviata", + content: { + "application/json": { + schema: { + type: "object", + properties: { + success: { type: "boolean" }, + message: { type: "string" } + }, + }, + }, + }, + }, + }, + }, + }, + "/dataset/status": { + get: { + summary: "Ottieni lo stato della registrazione dataset", + responses: { + 200: { + description: "Stato corrente", + content: { + "application/json": { + schema: { + type: "object", + properties: { + isRecording: { type: "boolean" }, + recordCount: { type: "number" } + }, + }, + }, + }, + }, + }, + }, + }, + "/dataset/files": { + get: { + summary: "Ottieni la lista dei file log salvati", + responses: { + 200: { + description: "Lista file log", + content: { + "application/json": { + schema: { + type: "object", + properties: { + files: { + type: "array", + items: { + type: "object", + properties: { + name: { type: "string" }, + size: { type: "number" }, + created: { type: "string" }, + modified: { type: "string" } + } + } + }, + count: { type: "number" } + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; +} + +module.exports = { setupRoutes, getOpenApiSpec }; \ No newline at end of file diff --git a/plugin/tools/utils.js b/plugin/tools/utils.js new file mode 100644 index 0000000..5f8e339 --- /dev/null +++ b/plugin/tools/utils.js @@ -0,0 +1,78 @@ +const fs = require('fs'); +const path = require('path'); + + +//Ottieni il percodi dal nome di una cartella, se questa non esiste, viene creata +function getDirectory(directoryName) { + const directoryPath = path.resolve(__dirname, directoryName); + + if (!fs.existsSync(directoryPath)) { + fs.mkdirSync(directoryPath, { recursive: true }); + } else { + return directoryPath; + } +} + +/** + * Scrivi un file con + * @param {string} fileName - Il nome del file. + * @param {string} extension - L'estensione + * @param {string} content - Il contenuto del file. + * @param {string} inDirectory - Il percorso in cui scrivere il file. Se non viene specificato, il file verrà aggiunto alla cartella principale del server. + * + * 🧠 Esempio d’uso + * (async () => { + * await writeFileToFolder("data", "prova.json", JSON.stringify({ name: "Giuseppe", age: 17 }, null, 2)); + * })(); + * + */ +async function write(fileName, extension, content, inDirectory) { + try { + const directoryPath = inDirectory ? getDirectory(inDirectory) : path.resolve(__dirname, '..'); + fs.mkdirSync(directoryPath, {recursive: true}); + + const filePath = path.join(directoryPath, `${fileName}.${extension}`); + await fs.writeFileSync(filePath, content, 'utf-8'); + } catch (error) { + console.error(`Error writing file ${fileName}.${extension}:`, error); + } +} + +//Funzione per ottenere la data nel formato dd/mm/yyyy hh:mm +function getDate(isoString) { + const date = new Date(isoString); + + const day = String(date.getDate()).padStart(2, "0"); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const year = date.getFullYear(); + + const hours = String(date.getHours()).padStart(2, "0"); + const minutes = String(date.getMinutes()).padStart(2, "0"); + + return `${day}/${month}/${year} ${hours}:${minutes}`; +} + +// Funzione per ottenere il tempo relativo ("2 ore fa", "tra 4 ore") +function relativeData(isoString) { + const date = new Date(isoString); + const now = new Date(); + const diffMs = date - now; // differenza in millisecondi + const diffSec = Math.round(diffMs / 1000); + const diffMin = Math.round(diffSec / 60); + const diffHr = Math.round(diffMin / 60); + const diffDay = Math.round(diffHr / 24); + + const rtf = new Intl.RelativeTimeFormat("it", { numeric: "auto" }); + + if (Math.abs(diffSec) < 60) return rtf.format(diffSec, "second"); + if (Math.abs(diffMin) < 60) return rtf.format(diffMin, "minute"); + if (Math.abs(diffHr) < 24) return rtf.format(diffHr, "hour"); + return rtf.format(diffDay, "day"); +} + +module.exports = { + getDirectory, + write, + getDate, + relativeData, +} \ No newline at end of file