mirror of
https://github.com/openmhz/trunk-server.git
synced 2026-11-14 17:58:02 -05:00
Merge branch 'firehose'
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@ REACT_APP_FRONTEND_SERVER="${PROTOCOL}${DOMAIN_NAME}"
|
|||||||
REACT_APP_ACCOUNT_SERVER="${PROTOCOL}account.${DOMAIN_NAME}"
|
REACT_APP_ACCOUNT_SERVER="${PROTOCOL}account.${DOMAIN_NAME}"
|
||||||
REACT_APP_ADMIN_SERVER="${PROTOCOL}admin.${DOMAIN_NAME}"
|
REACT_APP_ADMIN_SERVER="${PROTOCOL}admin.${DOMAIN_NAME}"
|
||||||
REACT_APP_COOKIE_DOMAIN=".${DOMAIN_NAME}"
|
REACT_APP_COOKIE_DOMAIN=".${DOMAIN_NAME}"
|
||||||
REACT_APP_ADMIN_EMAIL=luke@robotastic.com
|
REACT_APP_ADMIN_EMAIL=support@openmhz.com
|
||||||
REACT_APP_SITE_NAME=OpenMHz
|
REACT_APP_SITE_NAME=OpenMHz
|
||||||
REACT_APP_ARCHIVE_DAYS=30
|
REACT_APP_ARCHIVE_DAYS=30
|
||||||
STRIPE_PUBLISHABLE_KEY=""
|
STRIPE_PUBLISHABLE_KEY=""
|
||||||
@@ -25,7 +25,7 @@ ADMIN_SERVER="${PROTOCOL}admin.${DOMAIN_NAME}"
|
|||||||
ACCOUNT_SERVER="${PROTOCOL}account.${DOMAIN_NAME}"
|
ACCOUNT_SERVER="${PROTOCOL}account.${DOMAIN_NAME}"
|
||||||
COOKIE_DOMAIN=".${DOMAIN_NAME}"
|
COOKIE_DOMAIN=".${DOMAIN_NAME}"
|
||||||
SITE_NAME="OpenMHz"
|
SITE_NAME="OpenMHz"
|
||||||
ADMIN_EMAIL="luke@robotastic.com"
|
ADMIN_EMAIL="support@openmhz.com"
|
||||||
S3_PROFILE='wasabi-account'
|
S3_PROFILE='wasabi-account'
|
||||||
S3_ENPOINT='s3.us-west-1.wasabisys.com'
|
S3_ENPOINT='s3.us-west-1.wasabisys.com'
|
||||||
FREE_PLAN=0
|
FREE_PLAN=0
|
||||||
|
|||||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,6 +1,6 @@
|
|||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
github: robotastic
|
github: # Replace with a single Github username
|
||||||
patreon: # Replace with a single Patreon username
|
patreon: # Replace with a single Patreon username
|
||||||
open_collective: # Replace with a single Open Collective username
|
open_collective: # Replace with a single Open Collective username
|
||||||
ko_fi: # Replace with a single Ko-fi username
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ const bodyParser = require("body-parser");
|
|||||||
const MongoStore = require("connect-mongo");
|
const MongoStore = require("connect-mongo");
|
||||||
const secrets = require("./secrets");
|
const secrets = require("./secrets");
|
||||||
|
|
||||||
var cookie_domain = process.env['REACT_APP_COOKIE_DOMAIN'] != null ? process.env['REACT_APP_COOKIE_DOMAIN'] : '.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
var cookie_domain = process.env['REACT_APP_COOKIE_DOMAIN'] != null ? process.env['REACT_APP_COOKIE_DOMAIN'] : '.openmhz.com';
|
||||||
var backend_server = process.env['REACT_APP_BACKEND_SERVER'] != null ? process.env['REACT_APP_BACKEND_SERVER'] : 'https://api.openmhz.com';
|
var backend_server = process.env['REACT_APP_BACKEND_SERVER'] != null ? process.env['REACT_APP_BACKEND_SERVER'] : 'https://api.openmhz.com';
|
||||||
var frontend_server = process.env['REACT_APP_FRONTEND_SERVER'] != null ? process.env['REACT_APP_FRONTEND_SERVER'] : 'https://openmhz.com';
|
var frontend_server = process.env['REACT_APP_FRONTEND_SERVER'] != null ? process.env['REACT_APP_FRONTEND_SERVER'] : 'https://openmhz.com';
|
||||||
var account_server = process.env['REACT_APP_ACCOUNT_SERVER'] != null ? process.env['REACT_APP_ACCOUNT_SERVER'] : 'https://account.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
var account_server = process.env['REACT_APP_ACCOUNT_SERVER'] != null ? process.env['REACT_APP_ACCOUNT_SERVER'] : 'https://account.openmhz.com';
|
||||||
var admin_server = process.env['REACT_APP_ADMIN_SERVER'] != null ? process.env['REACT_APP_ADMIN_SERVER'] : 'https://admin.openmhz.com';
|
var admin_server = process.env['REACT_APP_ADMIN_SERVER'] != null ? process.env['REACT_APP_ADMIN_SERVER'] : 'https://admin.openmhz.com';
|
||||||
var account_dev_server = "http://account.openmhz.test:3000"
|
var account_dev_server = "http://account.openmhz.test:3000"
|
||||||
var admin_dev_server = "http://admin.openmhz.test:3000"
|
var admin_dev_server = "http://admin.openmhz.test:3000"
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ const Mailjet = require('node-mailjet');
|
|||||||
|
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
|
|
||||||
var admin_email = process.env['REACT_APP_ADMIN_EMAIL'] != null ? process.env['REACT_APP_ADMIN_EMAIL'] : "luke@openmhz.com";
|
var admin_email = process.env['REACT_APP_ADMIN_EMAIL'] != null ? process.env['REACT_APP_ADMIN_EMAIL'] : "support@openmhz.com";
|
||||||
var site_name = process.env['REACT_APP_SITE_NAME'] != null ? process.env['REACT_APP_SITE_NAME'] : "OpenMHz";
|
var site_name = process.env['REACT_APP_SITE_NAME'] != null ? process.env['REACT_APP_SITE_NAME'] : "OpenMHz";
|
||||||
var account_server = process.env['REACT_APP_ACCOUNT_SERVER'] != null ? process.env['REACT_APP_ACCOUNT_SERVER'] : "https://account.openmhz.com";
|
var account_server = process.env['REACT_APP_ACCOUNT_SERVER'] != null ? process.env['REACT_APP_ACCOUNT_SERVER'] : "https://account.openmhz.com";
|
||||||
var cookie_domain = process.env['REACT_APP_COOKIE_DOMAIN'] != null ? process.env['REACT_APP_COOKIE_DOMAIN'] : '.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
var cookie_domain = process.env['REACT_APP_COOKIE_DOMAIN'] != null ? process.env['REACT_APP_COOKIE_DOMAIN'] : '.openmhz.com';
|
||||||
|
|
||||||
|
|
||||||
const mailjet = new Mailjet({
|
const mailjet = new Mailjet({
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ const isDev = process.env.NODE_ENV === "development"
|
|||||||
|
|
||||||
|
|
||||||
var frontend_server = process.env['REACT_APP_FRONTEND_SERVER'] != null ? process.env['REACT_APP_FRONTEND_SERVER'] : 'https://openmhz.com';
|
var frontend_server = process.env['REACT_APP_FRONTEND_SERVER'] != null ? process.env['REACT_APP_FRONTEND_SERVER'] : 'https://openmhz.com';
|
||||||
var account_server = process.env['REACT_APP_ACCOUNT_SERVER'] != null ? process.env['REACT_APP_ACCOUNT_SERVER'] : 'https://account.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
var account_server = process.env['REACT_APP_ACCOUNT_SERVER'] != null ? process.env['REACT_APP_ACCOUNT_SERVER'] : 'https://account.openmhz.com';
|
||||||
var admin_server = process.env['REACT_APP_ADMIN_SERVER'] != null ? process.env['REACT_APP_ADMIN_SERVER'] : 'https://admin.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
var admin_server = process.env['REACT_APP_ADMIN_SERVER'] != null ? process.env['REACT_APP_ADMIN_SERVER'] : 'https://admin.openmhz.com';
|
||||||
|
|
||||||
|
|
||||||
// -------------------------------------------
|
// -------------------------------------------
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ const Login = (props) => {
|
|||||||
<Header size="large">Let's get this started!</Header>
|
<Header size="large">Let's get this started!</Header>
|
||||||
<Header size="small">
|
<Header size="small">
|
||||||
{process.env.REACT_APP_SITE_NAME} makes it easy to share recordings from{" "}
|
{process.env.REACT_APP_SITE_NAME} makes it easy to share recordings from{" "}
|
||||||
<a href="https://github.com/robotastic/trunk-recorder">
|
<a href="https://github.com/TrunkRecorder/trunk-recorder">
|
||||||
Trunk Recorder
|
Trunk Recorder
|
||||||
</a>.</Header>
|
</a>.</Header>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ function Register(props) {
|
|||||||
<Message.Content>
|
<Message.Content>
|
||||||
<Message.Header>Welcome!</Message.Header>
|
<Message.Header>Welcome!</Message.Header>
|
||||||
<p>Creating an account on OpenMHz is the first step in sharing radio system recordings using Trunk Recorder.</p>
|
<p>Creating an account on OpenMHz is the first step in sharing radio system recordings using Trunk Recorder.</p>
|
||||||
<p>After creating an account, head over <a href="https://github.com/robotastic/trunk-recorder">here</a> to learn more about Trunk Recorder</p>
|
<p>After creating an account, head over <a href="https://github.com/TrunkRecorder/trunk-recorder">here</a> to learn more about Trunk Recorder</p>
|
||||||
<p> - Luke</p>
|
<p> - Luke</p>
|
||||||
</Message.Content>
|
</Message.Content>
|
||||||
</Message>
|
</Message>
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ You agree not to sell, resell, or offer for any commercial purposes, any portion
|
|||||||
|
|
||||||
<Header as='h2'>5. API Access</Header>
|
<Header as='h2'>5. API Access</Header>
|
||||||
|
|
||||||
The OpenMHz API is designed solely to support the OpenMHz website frontend and iOS app. You may not use the API for any other purpose without explicit permission from OpenMHz. The infrastructure for OpenMHz has not been well designed, and unauthorized use may result in the whole thing crashing. Additionally, some API calls may place a significant load on the server, and preplanning is required to avoid this. We will allow for API access on a case-by-case basis. Please contact us at: luke@robotastic.com
|
The OpenMHz API is designed solely to support the OpenMHz website frontend and iOS app. You may not use the API for any other purpose without explicit permission from OpenMHz. The infrastructure for OpenMHz has not been well designed, and unauthorized use may result in the whole thing crashing. Additionally, some API calls may place a significant load on the server, and preplanning is required to avoid this. We will allow for API access on a case-by-case basis. Please contact us at: support@openmhz.com
|
||||||
|
|
||||||
<Header as='h2'>6. Legal Compliance</Header>
|
<Header as='h2'>6. Legal Compliance</Header>
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ The Services are provided as a labor of love by a single individual. I have a li
|
|||||||
|
|
||||||
<Header as='h2'>14. Contact Me</Header>
|
<Header as='h2'>14. Contact Me</Header>
|
||||||
|
|
||||||
If you have any questions or concerns about these Terms, please contact me at luke@robotastic.com.
|
If you have any questions or concerns about these Terms, please contact me at support@openmhz.com.
|
||||||
|
|
||||||
By using OpenMHz, you acknowledge that you have read, understood, and agree to be bound by these Terms of Service. Thank you for using our Services!
|
By using OpenMHz, you acknowledge that you have read, understood, and agree to be bound by these Terms of Service. Thank you for using our Services!
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ const bodyParser = require("body-parser");
|
|||||||
const MongoStore = require("connect-mongo");
|
const MongoStore = require("connect-mongo");
|
||||||
const secrets = require("./secrets");
|
const secrets = require("./secrets");
|
||||||
|
|
||||||
var cookie_domain = process.env['REACT_APP_COOKIE_DOMAIN'] != null ? process.env['REACT_APP_COOKIE_DOMAIN'] : '.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
var cookie_domain = process.env['REACT_APP_COOKIE_DOMAIN'] != null ? process.env['REACT_APP_COOKIE_DOMAIN'] : '.openmhz.com';
|
||||||
var backend_server = process.env['REACT_APP_BACKEND_SERVER'] != null ? process.env['REACT_APP_BACKEND_SERVER'] : 'https://api.openmhz.com';
|
var backend_server = process.env['REACT_APP_BACKEND_SERVER'] != null ? process.env['REACT_APP_BACKEND_SERVER'] : 'https://api.openmhz.com';
|
||||||
var frontend_server = process.env['REACT_APP_FRONTEND_SERVER'] != null ? process.env['REACT_APP_FRONTEND_SERVER'] : 'https://openmhz.com';
|
var frontend_server = process.env['REACT_APP_FRONTEND_SERVER'] != null ? process.env['REACT_APP_FRONTEND_SERVER'] : 'https://openmhz.com';
|
||||||
var account_server = process.env['REACT_APP_ACCOUNT_SERVER'] != null ? process.env['REACT_APP_ACCOUNT_SERVER'] : 'https://account.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
var account_server = process.env['REACT_APP_ACCOUNT_SERVER'] != null ? process.env['REACT_APP_ACCOUNT_SERVER'] : 'https://account.openmhz.com';
|
||||||
var dev_server = "http://admin.openmhz.test:3000"
|
var dev_server = "http://admin.openmhz.test:3000"
|
||||||
|
|
||||||
module.exports = function(app, passport) {
|
module.exports = function(app, passport) {
|
||||||
|
|||||||
@@ -319,10 +319,14 @@ exports.uniqueShortName = async function (req, res, next) {
|
|||||||
res.status(500)
|
res.status(500)
|
||||||
res.json({
|
res.json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Short Name not provided in form"
|
message: "Shortname not provided in form"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shortName = shortName.replace(/[^\w]/gi, '');
|
||||||
|
shortName = shortName.toLowerCase();
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var system = await System.findOne({
|
var system = await System.findOne({
|
||||||
shortName: shortName.toLowerCase()
|
shortName: shortName.toLowerCase()
|
||||||
@@ -333,7 +337,7 @@ exports.uniqueShortName = async function (req, res, next) {
|
|||||||
res.status(500)
|
res.status(500)
|
||||||
res.json({
|
res.json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Short Name already in use"
|
message: "Shortname already in use"
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const About = () => {
|
|||||||
return (
|
return (
|
||||||
<Container text>
|
<Container text>
|
||||||
<Header size='large'>Share the Air</Header>
|
<Header size='large'>Share the Air</Header>
|
||||||
<p>{process.env.REACT_APP_SITE_NAME} makes it easy to share and archive recordings from <a href="https://github.com/robotastic/trunk-recorder">Trunk Recorder</a>.
|
<p>{process.env.REACT_APP_SITE_NAME} makes it easy to share and archive recordings from <a href="https://github.com/TrunkRecorder/trunk-recorder">Trunk Recorder</a>.
|
||||||
Sharing access to these radio systems allows for other members of your community to follow local events.
|
Sharing access to these radio systems allows for other members of your community to follow local events.
|
||||||
Raising awareness of what our local fire, police and EMS have to go through everyday should
|
Raising awareness of what our local fire, police and EMS have to go through everyday should
|
||||||
lead to greater appreciation for all the work they do that goes largely unseen.</p>
|
lead to greater appreciation for all the work they do that goes largely unseen.</p>
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const opentelemetry = require('@opentelemetry/sdk-node');
|
|
||||||
const { MongoDBInstrumentation } = require('@opentelemetry/instrumentation-mongodb');
|
|
||||||
const { TraceIdRatioBasedSampler } = require('@opentelemetry/sdk-trace-node');
|
|
||||||
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
|
|
||||||
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
|
|
||||||
const { diag, DiagConsoleLogger, DiagLogLevel } = require('@opentelemetry/api');
|
|
||||||
const {
|
|
||||||
Detector,
|
|
||||||
DetectorSync,
|
|
||||||
IResource,
|
|
||||||
ResourceDetectionConfig,
|
|
||||||
envDetectorSync,
|
|
||||||
hostDetectorSync,
|
|
||||||
processDetectorSync,
|
|
||||||
} = require("@opentelemetry/resources")
|
|
||||||
// const {
|
|
||||||
// dockerCGroupV1Detector,
|
|
||||||
// } = require('@opentelemetry/resource-detector-docker');
|
|
||||||
|
|
||||||
// For troubleshooting, set the log level to DiagLogLevel.DEBUG
|
|
||||||
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.WARN);
|
|
||||||
|
|
||||||
console.log("Starting OpenTelemetry Node SDK");
|
|
||||||
console.log("OTEL_EXPORTER_OTLP_ENDPOINT: " + process.env.OTEL_EXPORTER_OTLP_ENDPOINT);
|
|
||||||
console.log("OTEL_EXPORTER_OTLP_HEADERS: " + process.env.OTEL_EXPORTER_OTLP_HEADERS);
|
|
||||||
console.log("OTEL_SERVICE_NAME: " + process.env.OTEL_SERVICE_NAME);
|
|
||||||
|
|
||||||
|
|
||||||
// from https://github.com/open-telemetry/opentelemetry-js/issues/4638
|
|
||||||
// this is a workaround to wait for async attributes to be resolved
|
|
||||||
function awaitAttributes(detector){
|
|
||||||
return {
|
|
||||||
async detect(config) {
|
|
||||||
const resource = detector.detect(config)
|
|
||||||
await resource.waitForAsyncAttributes()
|
|
||||||
|
|
||||||
return resource
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const samplePercentage = 0.05;
|
|
||||||
|
|
||||||
const sdk = new opentelemetry.NodeSDK({
|
|
||||||
traceExporter: new OTLPTraceExporter(),
|
|
||||||
sampler: new TraceIdRatioBasedSampler(samplePercentage),
|
|
||||||
instrumentations: [
|
|
||||||
new MongoDBInstrumentation({
|
|
||||||
// see under for available configuration
|
|
||||||
}),
|
|
||||||
getNodeAutoInstrumentations({
|
|
||||||
// we recommend disabling fs autoinstrumentation since it can be noisy
|
|
||||||
// and expensive during startup
|
|
||||||
'@opentelemetry/instrumentation-fs': {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
resourceDetectors: [
|
|
||||||
awaitAttributes(envDetectorSync),
|
|
||||||
awaitAttributes(processDetectorSync),
|
|
||||||
awaitAttributes(hostDetectorSync),
|
|
||||||
],
|
|
||||||
//resourceDetectors: [dockerCGroupV1Detector],
|
|
||||||
});
|
|
||||||
|
|
||||||
sdk.start();
|
|
||||||
@@ -4,7 +4,7 @@ var bodyParser = require("body-parser");
|
|||||||
|
|
||||||
var backend_server = process.env['REACT_APP_BACKEND_SERVER'] != null ? process.env['REACT_APP_BACKEND_SERVER'] : 'https://api.openmhz.com';
|
var backend_server = process.env['REACT_APP_BACKEND_SERVER'] != null ? process.env['REACT_APP_BACKEND_SERVER'] : 'https://api.openmhz.com';
|
||||||
var frontend_server = process.env['REACT_APP_FRONTEND_SERVER'] != null ? process.env['REACT_APP_FRONTEND_SERVER'] : 'https://openmhz.com';
|
var frontend_server = process.env['REACT_APP_FRONTEND_SERVER'] != null ? process.env['REACT_APP_FRONTEND_SERVER'] : 'https://openmhz.com';
|
||||||
var admin_server = process.env['REACT_APP_ADMIN_SERVER'] != null ? process.env['REACT_APP_ADMIN_SERVER'] : 'https://admin.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
var admin_server = process.env['REACT_APP_ADMIN_SERVER'] != null ? process.env['REACT_APP_ADMIN_SERVER'] : 'https://admin.openmhz.com';
|
||||||
var dev_server = frontend_server + ":3000"
|
var dev_server = frontend_server + ":3000"
|
||||||
|
|
||||||
|
|
||||||
@@ -42,7 +42,9 @@ module.exports = function(app) {
|
|||||||
res.setHeader('Access-Control-Allow-Origin', origin);
|
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||||
} else if (req.headers["user-agent"] == 'TrunkRecorder1.0') {
|
} else if (req.headers["user-agent"] == 'TrunkRecorder1.0') {
|
||||||
res.setHeader('Access-Control-Allow-Origin', "*");
|
res.setHeader('Access-Control-Allow-Origin', "*");
|
||||||
} else {
|
} else if (origin &&origin.indexOf(".vercel.app") > -1) {
|
||||||
|
next("Scraper!! ☠️ " + origin);
|
||||||
|
} else {
|
||||||
res.setHeader('Access-Control-Allow-Origin', "*");
|
res.setHeader('Access-Control-Allow-Origin', "*");
|
||||||
if (origin) {
|
if (origin) {
|
||||||
console.warn("forcing CORS for: " + origin + " referer: " + req.headers.referer + " url: " + req.originalUrl);
|
console.warn("forcing CORS for: " + origin + " referer: " + req.headers.referer + " url: " + req.originalUrl);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
const { ObjectId } = require('mongodb');
|
const { ObjectId } = require('mongodb');
|
||||||
const Call = require("../models/call");
|
const Call = require("../models/call");
|
||||||
const Group = require("../models/group");
|
const Group = require("../models/group");
|
||||||
const opentelemetry = require('@opentelemetry/api');
|
|
||||||
var defaultNumResults = 50;
|
var defaultNumResults = 50;
|
||||||
|
|
||||||
var channels = {};
|
var channels = {};
|
||||||
@@ -17,10 +16,12 @@ const build_call_list = (items) => {
|
|||||||
url: item.url,
|
url: item.url,
|
||||||
filename: item.path + item.name,
|
filename: item.path + item.name,
|
||||||
time: item.time,
|
time: item.time,
|
||||||
srcList: item.srcList,
|
// Lean results skip schema defaults, so older documents can lack these.
|
||||||
star: item.star,
|
srcList: item.srcList ?? [],
|
||||||
|
star: item.star ?? 0,
|
||||||
|
emergency: item.emergency,
|
||||||
freq: item.freq,
|
freq: item.freq,
|
||||||
patches: item.patches,
|
patches: item.patches ?? [],
|
||||||
len: Math.round(item.len)
|
len: Math.round(item.len)
|
||||||
};
|
};
|
||||||
calls.push(call);
|
calls.push(call);
|
||||||
@@ -37,6 +38,7 @@ async function get_calls(query, numResults, middleDate, res) {
|
|||||||
path: true,
|
path: true,
|
||||||
name: true,
|
name: true,
|
||||||
time: true,
|
time: true,
|
||||||
|
emergency: true,
|
||||||
srcList: true,
|
srcList: true,
|
||||||
freq: true,
|
freq: true,
|
||||||
patches: true,
|
patches: true,
|
||||||
@@ -47,7 +49,7 @@ async function get_calls(query, numResults, middleDate, res) {
|
|||||||
|
|
||||||
const sort = { length: -1 };
|
const sort = { length: -1 };
|
||||||
try {
|
try {
|
||||||
const items = await Call.find(query.filter, fields).sort(query.sort_order).limit(numResults);
|
const items = await Call.find(query.filter, fields).sort(query.sort_order).limit(numResults).lean();
|
||||||
const refined_items = build_call_list(items);
|
const refined_items = build_call_list(items);
|
||||||
calls.push(...refined_items);
|
calls.push(...refined_items);
|
||||||
|
|
||||||
@@ -57,7 +59,7 @@ async function get_calls(query, numResults, middleDate, res) {
|
|||||||
$gt: middleDate
|
$gt: middleDate
|
||||||
};
|
};
|
||||||
|
|
||||||
const items = await Call.find(query.filter, fields).sort(query.sort_order).limit(numResults);
|
const items = await Call.find(query.filter, fields).sort(query.sort_order).limit(numResults).lean();
|
||||||
const refined_items = build_call_list(items);
|
const refined_items = build_call_list(items);
|
||||||
calls.push(...refined_items);
|
calls.push(...refined_items);
|
||||||
}
|
}
|
||||||
@@ -210,11 +212,13 @@ function package_call(item) {
|
|||||||
time: item.time,
|
time: item.time,
|
||||||
timeString: timeString,
|
timeString: timeString,
|
||||||
dateString: dateString,
|
dateString: dateString,
|
||||||
|
patches: item.patches,
|
||||||
path: item.path,
|
path: item.path,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
freq: item.freq,
|
freq: item.freq,
|
||||||
srcList: item.srcList,
|
srcList: item.srcList,
|
||||||
star: item.star,
|
star: item.star,
|
||||||
|
emergency: item.emergency,
|
||||||
len: Math.round(item.len)
|
len: Math.round(item.len)
|
||||||
};
|
};
|
||||||
return call;
|
return call;
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ var s3_profile = process.env['S3_PROFILE'] != null ? process.env['S3_PROFILE'] :
|
|||||||
|
|
||||||
const client = new S3Client({
|
const client = new S3Client({
|
||||||
credentials: fromIni({ profile: s3_profile }),
|
credentials: fromIni({ profile: s3_profile }),
|
||||||
|
requestChecksumCalculation: 'WHEN_REQUIRED',
|
||||||
|
responseChecksumValidation: 'WHEN_REQUIRED',
|
||||||
endpoint: s3_endpoint,
|
endpoint: s3_endpoint,
|
||||||
region: s3_region,
|
region: s3_region,
|
||||||
maxAttempts: 2
|
maxAttempts: 2
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ var System = require("../models/system");
|
|||||||
const Mailjet = require('node-mailjet');
|
const Mailjet = require('node-mailjet');
|
||||||
var schedule = require('node-schedule');
|
var schedule = require('node-schedule');
|
||||||
|
|
||||||
var admin_email = process.env['REACT_APP_ADMIN_EMAIL'] != null ? process.env['REACT_APP_ADMIN_EMAIL'] : "luke@openmhz.com";
|
var admin_email = process.env['REACT_APP_ADMIN_EMAIL'] != null ? process.env['REACT_APP_ADMIN_EMAIL'] : "support@openmhz.com";
|
||||||
var admin_server = process.env['REACT_APP_ADMIN_SERVER'] != null ? process.env['REACT_APP_ADMIN_SERVER'] : "https://admin.openmhz.com";
|
var admin_server = process.env['REACT_APP_ADMIN_SERVER'] != null ? process.env['REACT_APP_ADMIN_SERVER'] : "https://admin.openmhz.com";
|
||||||
var site_name = process.env['REACT_APP_SITE_NAME'] != null ? process.env['REACT_APP_SITE_NAME'] : "OpenMHz";
|
var site_name = process.env['REACT_APP_SITE_NAME'] != null ? process.env['REACT_APP_SITE_NAME'] : "OpenMHz";
|
||||||
|
|
||||||
@@ -147,6 +147,31 @@ async function load_systems(systemClients) {
|
|||||||
// console.log("Loaded Systems: " + systemList.length + " that have been active since: " + fromDate);
|
// console.log("Loaded Systems: " + systemList.length + " that have been active since: " + fromDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.get_system_status = async function (req, res) {
|
||||||
|
var shortName = req.params.shortName.toLowerCase();
|
||||||
|
var system = systemList.find(function (element) {
|
||||||
|
return element.shortName == shortName;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!system) {
|
||||||
|
res.status(404);
|
||||||
|
res.send("System not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var now_string = new Date().toLocaleString();
|
||||||
|
var last_active_string = new Date(system.lastActive).toLocaleString();
|
||||||
|
var response = {
|
||||||
|
name: system.name,
|
||||||
|
description: system.description,
|
||||||
|
active: system.active,
|
||||||
|
lastActive: last_active_string,
|
||||||
|
clientCount: system.clientCount,
|
||||||
|
callAvg: system.callAvg,
|
||||||
|
requestTime: now_string
|
||||||
|
}
|
||||||
|
res.contentType('json');
|
||||||
|
res.send(JSON.stringify(response));
|
||||||
|
}
|
||||||
|
|
||||||
exports.get_systems = async function (req, res) {
|
exports.get_systems = async function (req, res) {
|
||||||
/* going to use a cron job to update the system list every 2 minutes
|
/* going to use a cron job to update the system list every 2 minutes
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const opentelemetry = require('@opentelemetry/api');
|
|
||||||
const sysStats = require("../sys_stats");
|
const sysStats = require("../sys_stats");
|
||||||
const systemSchema = require("../models/systemSchema");
|
const systemSchema = require("../models/systemSchema");
|
||||||
const talkgroupSchema = require("../models/talkgroupSchema");
|
const talkgroupSchema = require("../models/talkgroupSchema");
|
||||||
const callSchema = require("../models/callSchema");
|
const callSchema = require("../models/callSchema");
|
||||||
const { trace, context } = opentelemetry;
|
|
||||||
const mongoose = require("mongoose");
|
const mongoose = require("mongoose");
|
||||||
const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3");
|
const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3");
|
||||||
const { fromIni } = require("@aws-sdk/credential-providers");
|
const { fromIni } = require("@aws-sdk/credential-providers");
|
||||||
const { NodeHttpHandler } = require('@smithy/node-http-handler');
|
const { NodeHttpHandler } = require('@smithy/node-http-handler');
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
|
|
||||||
|
// keepAlive matters: without it every upload does a full TCP + TLS handshake to S3.
|
||||||
const agent = new https.Agent({
|
const agent = new https.Agent({
|
||||||
|
keepAlive: true,
|
||||||
maxSockets: 250,
|
maxSockets: 250,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,13 +52,34 @@ const client = new S3Client({
|
|||||||
httpsAgent: agent,
|
httpsAgent: agent,
|
||||||
}),
|
}),
|
||||||
credentials: fromIni({ profile: s3_profile }),
|
credentials: fromIni({ profile: s3_profile }),
|
||||||
|
requestChecksumCalculation: 'WHEN_REQUIRED',
|
||||||
|
responseChecksumValidation: 'WHEN_REQUIRED',
|
||||||
endpoint: s3_endpoint,
|
endpoint: s3_endpoint,
|
||||||
region: s3_region,
|
region: s3_region,
|
||||||
maxAttempts: 2,
|
maxAttempts: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Every upload needs the system's key, so cache the lookup. A changed key or
|
||||||
|
// ignoreUnknownTalkgroup setting takes up to the TTL to apply. Only found systems
|
||||||
|
// are cached, so a newly created system works immediately.
|
||||||
|
const SYSTEM_CACHE_TTL_MS = 60 * 1000;
|
||||||
|
const systemCache = new Map();
|
||||||
|
|
||||||
|
async function getSystem(shortName) {
|
||||||
|
const cached = systemCache.get(shortName);
|
||||||
|
if (cached && cached.expires > Date.now()) {
|
||||||
|
return cached.system;
|
||||||
|
}
|
||||||
|
const system = await mongo_conn_fast.model("System").findOne({ shortName }, ["key", "ignoreUnknownTalkgroup"]).lean();
|
||||||
|
if (system) {
|
||||||
|
systemCache.set(shortName, { system, expires: Date.now() + SYSTEM_CACHE_TTL_MS });
|
||||||
|
} else {
|
||||||
|
systemCache.delete(shortName);
|
||||||
|
}
|
||||||
|
return system;
|
||||||
|
}
|
||||||
|
|
||||||
exports.upload = async function (req, res, next) {
|
exports.upload = async function (req, res, next) {
|
||||||
const tracer = trace.getTracer("upload-service");
|
|
||||||
let start_time = Date.now();
|
let start_time = Date.now();
|
||||||
let validateSystemTime = 0;
|
let validateSystemTime = 0;
|
||||||
let readFileTime = 0;
|
let readFileTime = 0;
|
||||||
@@ -66,217 +87,163 @@ exports.upload = async function (req, res, next) {
|
|||||||
let saveCallTime = 0;
|
let saveCallTime = 0;
|
||||||
let cleanupTime = 0;
|
let cleanupTime = 0;
|
||||||
let statsTime = 0;
|
let statsTime = 0;
|
||||||
return context.with(context.active(), async () => {
|
try {
|
||||||
const parentSpan = trace.getActiveSpan(context.active());
|
if (!req.file || !['.m4a', '.mp3'].includes(path.extname(req.file.originalname))) {
|
||||||
await tracer.startActiveSpan('upload_handler', { parent: parentSpan }, async (span) => {
|
console.warn(`[${req.params.shortName}] Error file name is wrong or file does not exist`);
|
||||||
try {
|
res.status(500).send("Error, invalid filename:\n");
|
||||||
span.setAttribute('call.shortName', req.params.shortName.toLowerCase());
|
return;
|
||||||
span.setAttribute('call.talkgroup_num', req.body.talkgroup_num);
|
}
|
||||||
span.setAttribute('call.start_time', req.body.start_time);
|
|
||||||
|
|
||||||
if (!req.file || !['.m4a', '.mp3'].includes(path.extname(req.file.originalname))) {
|
const shortName = req.params.shortName.toLowerCase();
|
||||||
console.warn(`[${req.params.shortName}] Error file name is wrong or file does not exist`);
|
const apiKey = req.body.api_key;
|
||||||
span.setStatus({
|
const talkgroupNum = parseInt(req.body.talkgroup_num);
|
||||||
code: opentelemetry.SpanStatusCode.ERROR,
|
const freq = parseFloat(req.body.freq);
|
||||||
message: "Invalid file or filename",
|
const time = new Date(parseInt(req.body.start_time) * 1000);
|
||||||
});
|
let stopTime = new Date();
|
||||||
res.status(500).send("Error, invalid filename:\n");
|
if (req.body.stop_time) {
|
||||||
return;
|
stopTime = new Date(parseInt(req.body.stop_time) * 1000);
|
||||||
}
|
}
|
||||||
|
const startTime = req.body.start_time;
|
||||||
|
const emergency = parseInt(req.body.emergency);
|
||||||
|
|
||||||
const shortName = req.params.shortName.toLowerCase();
|
let errorCount = parseInt(req.body.error_count) || 0;
|
||||||
const apiKey = req.body.api_key;
|
let spikeCount = parseInt(req.body.spike_count) || 0;
|
||||||
const talkgroupNum = parseInt(req.body.talkgroup_num);
|
|
||||||
const freq = parseFloat(req.body.freq);
|
|
||||||
const time = new Date(parseInt(req.body.start_time) * 1000);
|
|
||||||
let stopTime = new Date();
|
|
||||||
if (req.body.stop_time) {
|
|
||||||
stopTime = new Date(parseInt(req.body.stop_time) * 1000);
|
|
||||||
}
|
|
||||||
const startTime = req.body.start_time;
|
|
||||||
const emergency = parseInt(req.body.emergency);
|
|
||||||
|
|
||||||
let errorCount = parseInt(req.body.error_count) || 0;
|
let srcList = [];
|
||||||
let spikeCount = parseInt(req.body.spike_count) || 0;
|
try {
|
||||||
|
srcList = JSON.parse(req.body.source_list);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[${req.params.shortName}] Error /:shortName/upload Parsing Source/Freq List - Error: ${err}`);
|
||||||
|
res.status(500).send("Error parsing sourcelist " + err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let srcList = [];
|
patches = [];
|
||||||
try {
|
|
||||||
srcList = JSON.parse(req.body.source_list);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(`[${req.params.shortName}] Error /:shortName/upload Parsing Source/Freq List - Error: ${err}`);
|
|
||||||
res.status(500).send("Error parsing sourcelist " + err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
patches = [];
|
let req_patches;
|
||||||
|
req_patches = req.body.patch_list;
|
||||||
|
|
||||||
let req_patches;
|
if(typeof req_patches != "undefined"){
|
||||||
req_patches = req.body.patch_list;
|
var split_patches = req_patches.replace("[","").replace("]","").split(",");
|
||||||
|
|
||||||
if(typeof req_patches != "undefined"){
|
for (var patch in split_patches){
|
||||||
var split_patches = req_patches.replace("[","").replace("]","").split(",");
|
patches.push(split_patches[patch]);
|
||||||
|
|
||||||
for (var patch in split_patches){
|
|
||||||
patches.push(split_patches[patch]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let item = null;
|
|
||||||
|
|
||||||
await tracer.startActiveSpan('validate_system', { parent: trace.getActiveSpan(context.active()) }, async (validateSpan) => {
|
|
||||||
try {
|
|
||||||
item = await mongo_conn_fast.model("System").findOne({ shortName }, ["key", "ignoreUnknownTalkgroup"]);
|
|
||||||
validateSpan.setAttribute('system.exists', !!item);
|
|
||||||
} catch (err) {
|
|
||||||
validateSpan.recordException(err);
|
|
||||||
validateSpan.setStatus({
|
|
||||||
code: opentelemetry.SpanStatusCode.ERROR,
|
|
||||||
message: "Error validating system: " + err.message,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
validateSpan.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!item) {
|
|
||||||
console.warn(`[${req.params.shortName}] Error /:shortName/upload ShortName does not exist`);
|
|
||||||
res.status(500).send("ShortName does not exist: " + shortName + "\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (apiKey !== item.key) {
|
|
||||||
console.warn(`[${req.params.shortName}] Error /:shortName/upload API Key Mismatch - Provided key: ${apiKey}`);
|
|
||||||
res.status(500).send("API Keys do not match!\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shortName === "hennearmer" && [3421, 3423].includes(talkgroupNum)) {
|
|
||||||
res.status(200).end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
validateSystemTime = Date.now();
|
|
||||||
if (item.ignoreUnknownTalkgroup) {
|
|
||||||
const talkgroupExists = await mongo_conn_fast.model("Talkgroup").exists({
|
|
||||||
shortName,
|
|
||||||
num: talkgroupNum,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!talkgroupExists) {
|
|
||||||
try {
|
|
||||||
fs.unlinkSync(req.file.path);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`[${call.shortName}] error deleting: ${req.file.path}`);
|
|
||||||
}
|
|
||||||
res.status(500).send("Talkgroup does not exist, skipping.\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).end();
|
|
||||||
|
|
||||||
const local_path = `/${shortName}/${time.getFullYear()}/${time.getMonth() + 1}/${time.getDate()}/`;
|
|
||||||
const object_key = `media/${shortName}/${talkgroupNum}/${shortName}-${talkgroupNum}-${startTime}${path.extname(req.file.originalname)}`;
|
|
||||||
const url = `${s3_public_url}/${object_key}`;
|
|
||||||
|
|
||||||
const call = new (mongo_conn_slow.model("Call"))({
|
|
||||||
shortName,
|
|
||||||
talkgroupNum,
|
|
||||||
objectKey: object_key,
|
|
||||||
endpoint: s3_endpoint,
|
|
||||||
bucket: s3_bucket,
|
|
||||||
time,
|
|
||||||
name: `${talkgroupNum}-${startTime}.m4a`,
|
|
||||||
freq,
|
|
||||||
errorCount,
|
|
||||||
spikeCount,
|
|
||||||
url,
|
|
||||||
emergency,
|
|
||||||
path: local_path,
|
|
||||||
patches: patches,
|
|
||||||
srcList,
|
|
||||||
len: req.body.call_length ? parseFloat(req.body.call_length) : (stopTime - time) / 1000,
|
|
||||||
});
|
|
||||||
|
|
||||||
let fileContent;
|
|
||||||
|
|
||||||
await tracer.startActiveSpan('upload_to_s3', { parent: trace.getActiveSpan(context.active()) }, async (uploadSpan) => {
|
|
||||||
try {
|
|
||||||
fileContent = fs.readFileSync(req.file.path);
|
|
||||||
readFileTime = Date.now();
|
|
||||||
const command = new PutObjectCommand({
|
|
||||||
Bucket: s3_bucket,
|
|
||||||
Key: object_key,
|
|
||||||
Body: fileContent,
|
|
||||||
ACL: 'public-read',
|
|
||||||
});
|
|
||||||
var result = await client.send(command);
|
|
||||||
if (result && result.$metadata.httpStatusCode !== 200) {
|
|
||||||
console.error(`[${shortName}] Upload Error status code: ${result.$metadata.httpStatusCode}`);
|
|
||||||
console.error(result);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
uploadSpan.recordException(err);
|
|
||||||
uploadSpan.setStatus({
|
|
||||||
code: opentelemetry.SpanStatusCode.ERROR,
|
|
||||||
message: "Upload Error: " + err.message,
|
|
||||||
});
|
|
||||||
console.warn(`[${call.shortName}] Upload Error: ${err}`);
|
|
||||||
} finally {
|
|
||||||
uploadSpan.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
uploadFileTime = Date.now();
|
|
||||||
await tracer.startActiveSpan('save_call', { parent: trace.getActiveSpan(context.active()) }, async (saveSpan) => {
|
|
||||||
try {
|
|
||||||
await call.save();
|
|
||||||
} catch (err) {
|
|
||||||
saveSpan.recordException(err);
|
|
||||||
saveSpan.setStatus({
|
|
||||||
code: opentelemetry.SpanStatusCode.ERROR,
|
|
||||||
message: "Error saving call: " + err.message,
|
|
||||||
});
|
|
||||||
console.warn(`[${call.shortName}] Error saving call: ${err}`);
|
|
||||||
} finally {
|
|
||||||
saveSpan.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
saveCallTime = Date.now();
|
|
||||||
sysStats.addCall(call.toObject());
|
|
||||||
statsTime = Date.now();
|
|
||||||
|
|
||||||
if (call.len >= 1) {
|
|
||||||
req.call = call.toObject();
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
||||||
await tracer.startActiveSpan('cleanup_temp_file', { parent: trace.getActiveSpan(context.active()) }, async (cleanupSpan) => {
|
|
||||||
try {
|
|
||||||
fs.unlinkSync(req.file.path);
|
|
||||||
cleanupSpan.setAttribute('file.deleted', true);
|
|
||||||
} catch (err) {
|
|
||||||
cleanupSpan.recordException(err);
|
|
||||||
console.warn("There was an Error deleting: " + req.file.path);
|
|
||||||
} finally {
|
|
||||||
cleanupSpan.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
cleanupTime = Date.now();
|
|
||||||
//console.log(`[${call.shortName}] \t Verify System: ${validateSystemTime - start_time} \t Read file: ${readFileTime - validateSystemTime} \t Upload: ${ uploadFileTime - validateSystemTime} \t Save: ${saveCallTime - uploadFileTime} \tStats: ${statsTime - saveCallTime}\tCleanup: ${cleanupTime - statsTime} \t\t Total: ${cleanupTime - start_time}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error processing call upload: " + error);
|
|
||||||
span.recordException(error);
|
|
||||||
span.setStatus({
|
|
||||||
code: opentelemetry.SpanStatusCode.ERROR,
|
|
||||||
message: error.message,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
const totalTime = Date.now() - start_time;
|
|
||||||
if (totalTime > 10000) {
|
|
||||||
console.warn(`[${req.params.shortName}] Slow Upload - Size: ${call.len} Verify System: ${validateSystemTime - start_time} Read file: ${readFileTime - validateSystemTime} Upload: ${ uploadFileTime - validateSystemTime} Save: ${saveCallTime - uploadFileTime} Stats: ${statsTime - saveCallTime} Cleanup: ${cleanupTime - statsTime} Total: ${totalTime}`);
|
|
||||||
}
|
|
||||||
span.end();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let item = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
item = await getSystem(shortName);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[${req.params.shortName}] Error validating system: ${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
console.warn(`[${req.params.shortName}] Error /:shortName/upload ShortName does not exist`);
|
||||||
|
res.status(500).send("ShortName does not exist: " + shortName + "\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (apiKey !== item.key) {
|
||||||
|
console.warn(`[${req.params.shortName}] Error /:shortName/upload API Key Mismatch - Provided key: ${apiKey}`);
|
||||||
|
res.status(500).send("API Keys do not match!\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shortName === "hennearmer" && [3421, 3423].includes(talkgroupNum)) {
|
||||||
|
res.status(200).end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
validateSystemTime = Date.now();
|
||||||
|
if (item.ignoreUnknownTalkgroup) {
|
||||||
|
const talkgroupExists = await mongo_conn_fast.model("Talkgroup").exists({
|
||||||
|
shortName,
|
||||||
|
num: talkgroupNum,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!talkgroupExists) {
|
||||||
|
res.status(500).send("Talkgroup does not exist, skipping.\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).end();
|
||||||
|
|
||||||
|
const local_path = `/${shortName}/${time.getFullYear()}/${time.getMonth() + 1}/${time.getDate()}/`;
|
||||||
|
const object_key = `media/${shortName}/${talkgroupNum}/${shortName}-${talkgroupNum}-${startTime}${path.extname(req.file.originalname)}`;
|
||||||
|
const url = `${s3_public_url}/${object_key}`;
|
||||||
|
|
||||||
|
const call = new (mongo_conn_slow.model("Call"))({
|
||||||
|
shortName,
|
||||||
|
talkgroupNum,
|
||||||
|
objectKey: object_key,
|
||||||
|
endpoint: s3_endpoint,
|
||||||
|
bucket: s3_bucket,
|
||||||
|
time,
|
||||||
|
name: `${talkgroupNum}-${startTime}.m4a`,
|
||||||
|
freq,
|
||||||
|
errorCount,
|
||||||
|
spikeCount,
|
||||||
|
url,
|
||||||
|
emergency,
|
||||||
|
path: local_path,
|
||||||
|
patches: patches,
|
||||||
|
srcList,
|
||||||
|
len: req.body.call_length ? parseFloat(req.body.call_length) : (stopTime - time) / 1000,
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
let fileContent;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fileContent = fs.readFileSync(req.file.path);
|
||||||
|
readFileTime = Date.now();
|
||||||
|
const command = new PutObjectCommand({
|
||||||
|
Bucket: s3_bucket,
|
||||||
|
Key: object_key,
|
||||||
|
Body: fileContent,
|
||||||
|
ACL: 'public-read',
|
||||||
|
});
|
||||||
|
var result = await client.send(command);
|
||||||
|
if (result && result.$metadata.httpStatusCode !== 200) {
|
||||||
|
console.error(`[${shortName}] Upload Error status code: ${result.$metadata.httpStatusCode}`);
|
||||||
|
console.error(result);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[${call.shortName}] Upload Error: ${err}`);
|
||||||
|
}
|
||||||
|
uploadFileTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await call.save();
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[${call.shortName}] Error saving call: ${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveCallTime = Date.now();
|
||||||
|
sysStats.addCall(call.toObject());
|
||||||
|
statsTime = Date.now();
|
||||||
|
|
||||||
|
if (call.len >= 1) {
|
||||||
|
req.call = call.toObject();
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log(`[${call.shortName}] \t Verify System: ${validateSystemTime - start_time} \t Read file: ${readFileTime - validateSystemTime} \t Upload: ${ uploadFileTime - validateSystemTime} \t Save: ${saveCallTime - uploadFileTime} \tStats: ${statsTime - saveCallTime}\tCleanup: ${cleanupTime - statsTime} \t\t Total: ${cleanupTime - start_time}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing call upload: " + error);
|
||||||
|
} finally {
|
||||||
|
// multer has already written the temp file, so remove it on every exit path.
|
||||||
|
if (req.file) {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(req.file.path);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("There was an Error deleting: " + req.file.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cleanupTime = Date.now();
|
||||||
|
const totalTime = Date.now() - start_time;
|
||||||
|
if (totalTime > 10000) {
|
||||||
|
console.warn(`[${req.params.shortName}] Slow Upload - Size: ${call.len} Verify System: ${validateSystemTime - start_time} Read file: ${readFileTime - validateSystemTime} Upload: ${ uploadFileTime - validateSystemTime} Save: ${saveCallTime - uploadFileTime} Stats: ${statsTime - saveCallTime} Cleanup: ${cleanupTime - statsTime} Total: ${totalTime}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
178
backend/index.js
178
backend/index.js
@@ -1,4 +1,3 @@
|
|||||||
const tracing = require("./agents/otel-tracing");
|
|
||||||
var express = require("express");
|
var express = require("express");
|
||||||
var configureExpress = require("./config/express");
|
var configureExpress = require("./config/express");
|
||||||
|
|
||||||
@@ -25,6 +24,12 @@ var app = express();
|
|||||||
|
|
||||||
const server = require('http').createServer(app);
|
const server = require('http').createServer(app);
|
||||||
|
|
||||||
|
// Must outlast nginx's upstream keepalive_timeout (60s), so nginx is always the
|
||||||
|
// side that closes an idle connection. Otherwise it can send a request on a
|
||||||
|
// connection Node has just closed, and nginx won't retry a POST.
|
||||||
|
server.keepAliveTimeout = 65000;
|
||||||
|
server.headersTimeout = 70000;
|
||||||
|
|
||||||
|
|
||||||
const io = require('socket.io')(server, {
|
const io = require('socket.io')(server, {
|
||||||
cors: {
|
cors: {
|
||||||
@@ -47,6 +52,7 @@ const mongo_host = typeof process.env['MONGO_HOST'] !== 'undefined' ? process.en
|
|||||||
const mongo_port = typeof process.env['MONGO_PORT'] !== 'undefined' ? process.env['MONGO_PORT'] : 27017;
|
const mongo_port = typeof process.env['MONGO_PORT'] !== 'undefined' ? process.env['MONGO_PORT'] : 27017;
|
||||||
const mongo_user = process.env['MONGO_USER'];
|
const mongo_user = process.env['MONGO_USER'];
|
||||||
const mongo_password = process.env['MONGO_PASSWORD'];
|
const mongo_password = process.env['MONGO_PASSWORD'];
|
||||||
|
const firehose_key = process.env['FIREHOSE_KEY'] !== 'undefined' ? process.env['FIREHOSE_KEY'] : null;
|
||||||
|
|
||||||
let mongoUrl;
|
let mongoUrl;
|
||||||
|
|
||||||
@@ -59,20 +65,20 @@ if ((typeof mongo_user !== 'undefined') && (typeof mongo_password !== 'undefined
|
|||||||
|
|
||||||
|
|
||||||
const connect = async () => {
|
const connect = async () => {
|
||||||
// Demonstrate the readyState and on event emitters
|
// Demonstrate the readyState and on event emitters
|
||||||
console.log(mongoose.connection.readyState); //logs 0
|
console.log(mongoose.connection.readyState); //logs 0
|
||||||
mongoose.connection.on('connecting', () => {
|
mongoose.connection.on('connecting', () => {
|
||||||
console.log('Mongoose is connecting')
|
console.log('Mongoose is connecting')
|
||||||
});
|
});
|
||||||
mongoose.connection.on('connected', () => {
|
mongoose.connection.on('connected', () => {
|
||||||
console.log('Mongoose is connected');
|
console.log('Mongoose is connected');
|
||||||
});
|
});
|
||||||
mongoose.connection.on('disconnecting', () => {
|
mongoose.connection.on('disconnecting', () => {
|
||||||
console.log('Mongoose is disconnecting');
|
console.log('Mongoose is disconnecting');
|
||||||
});
|
});
|
||||||
|
|
||||||
await mongoose.connect(mongoUrl, { useNewUrlParser: true, useUnifiedTopology: true, maxPoolSize: 50 });
|
await mongoose.connect(mongoUrl, { useNewUrlParser: true, useUnifiedTopology: true, maxPoolSize: 50 });
|
||||||
console.log("All Done");
|
console.log("All Done");
|
||||||
}
|
}
|
||||||
connect();
|
connect();
|
||||||
|
|
||||||
@@ -106,7 +112,6 @@ function addSystemClients(req, res, next) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addTotalClients(req, res, next) {
|
function addTotalClients(req, res, next) {
|
||||||
|
|
||||||
req.totalClients = Object.keys(clients).length;
|
req.totalClients = Object.keys(clients).length;
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
@@ -152,6 +157,9 @@ app.get('/events/:id', events.getEvent);
|
|||||||
app.get('/:shortName/stats', stats.get_stats);
|
app.get('/:shortName/stats', stats.get_stats);
|
||||||
app.get('/stats', addTotalClients, sys_stats.siteStats)
|
app.get('/stats', addTotalClients, sys_stats.siteStats)
|
||||||
|
|
||||||
|
/*------ SYSTEM STATUS ----------*/
|
||||||
|
app.get('/status/:shortName', systems.get_system_status);
|
||||||
|
|
||||||
|
|
||||||
function get_clients(req, res) {
|
function get_clients(req, res) {
|
||||||
if (req.params.shortName) {
|
if (req.params.shortName) {
|
||||||
@@ -190,62 +198,53 @@ function get_clients(req, res) {
|
|||||||
app.get('/clients', get_clients);
|
app.get('/clients', get_clients);
|
||||||
app.get('/:shortName/clients', get_clients);
|
app.get('/:shortName/clients', get_clients);
|
||||||
app.use(function (err, req, res, next) {
|
app.use(function (err, req, res, next) {
|
||||||
console.error("Caught an error");
|
|
||||||
console.error(err.stack);
|
console.log("Error: " + err);
|
||||||
/*
|
|
||||||
res.status(err.status || 500);
|
|
||||||
res.contentType('json');
|
|
||||||
res.send(JSON.stringify({
|
|
||||||
message: err.message,
|
|
||||||
error: err
|
|
||||||
}));*/
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function notify_clients(call) {
|
function notify_clients(call) {
|
||||||
call.type = "calls";
|
call.type = "calls";
|
||||||
|
const message = JSON.stringify(call);
|
||||||
var sent = 0;
|
var sent = 0;
|
||||||
|
|
||||||
for (var key in clients) {
|
for (var key in clients) {
|
||||||
if (clients.hasOwnProperty(key)) {
|
if (clients.hasOwnProperty(key)) {
|
||||||
var client = clients[key];
|
var client = clients[key];
|
||||||
if (client.active) {
|
if (client.active) {
|
||||||
if (client.shortName == call.shortName.toLowerCase()) {
|
if ((client.firehose == true) || (client.shortName == call.shortName.toLowerCase())) {
|
||||||
// if client is not filtering for stars, or if the client is filtering and the call has stars
|
// if client
|
||||||
if (!client.filterStarred || call.star) {
|
if ((client.firehose == true) || (client.filterType == "all")) {
|
||||||
if (client.filterCode == "") {
|
sent++;
|
||||||
sent++;
|
client.socket.emit("new message", message);
|
||||||
client.socket.emit("new message", JSON.stringify(call));
|
} else if (client.filterType == "unit") {
|
||||||
} else if (client.filterType == "unit") {
|
var codeArray = client.filterCode.split(',');
|
||||||
var codeArray = client.filterCode.split(',');
|
var success = false;
|
||||||
var success = false;
|
for (var j = 0; j < codeArray.length; ++j) {
|
||||||
for (var j = 0; j < codeArray.length; ++j) {
|
for (var k = 0; k < call.srcList.length; k++) {
|
||||||
for (var k = 0; k < call.srcList.length; k++) {
|
if (codeArray[j] == call.srcList[k]) {
|
||||||
if (codeArray[j] == call.srcList[k]) {
|
sent++;
|
||||||
sent++;
|
client.socket.emit("new message", message);
|
||||||
client.socket.emit("new message", JSON.stringify(call));
|
success = true;
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (success) {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (success) {
|
||||||
|
break;
|
||||||
} else {
|
}
|
||||||
var codeArray = client.talkgroupNums;
|
}
|
||||||
for (var j = 0; j < codeArray.length; ++j) {
|
} else {
|
||||||
if (codeArray[j] == call.talkgroupNum) {
|
var codeArray = client.talkgroupNums;
|
||||||
client.socket.emit("new message", JSON.stringify(call));
|
for (var j = 0; j < codeArray.length; ++j) {
|
||||||
sent++
|
if (codeArray[j] == call.talkgroupNum) {
|
||||||
break;
|
client.socket.emit("new message", message);
|
||||||
}
|
sent++
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,11 +255,30 @@ function notify_clients(call) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseHeader(header) {
|
||||||
|
for (const directive of header.split(",")[0].split(";")) {
|
||||||
|
if (directive.startsWith("for=")) {
|
||||||
|
return directive.substring(4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
io.sockets.on('connection', function (client) {
|
io.sockets.on('connection', function (client) {
|
||||||
|
const header_ipAddress = parseHeader(client.handshake.headers["forwarded"] || "");
|
||||||
|
const cf_ipAddress = client.handshake.headers["cf-connecting-ip"];
|
||||||
|
const x_ipAddress = client.handshake.headers["x-forwarded-for"]?.split(",")[0];
|
||||||
|
const ipAddress = cf_ipAddress || x_ipAddress || header_ipAddress;
|
||||||
clients[client.id] = { socket: client, active: false };
|
clients[client.id] = { socket: client, active: false };
|
||||||
clients[client.id].timestamp = new Date();
|
clients[client.id].timestamp = new Date();
|
||||||
client.on('start', async function (data) {
|
client.on('start', async function (data) {
|
||||||
if ((typeof clients[client.id] !== "undefined") && data.shortName) {
|
if (typeof clients[client.id] !== "undefined") {
|
||||||
|
if ((data.filterType != "firehose") && (!data.shortName || (typeof data.shortName != "string"))) { // If it is not firehose, then it must have a shortName
|
||||||
|
console.error("Error - Socket - Invalid ShortName: " + data.shortName + " ClientID: " + client.id + " IP: " + ipAddress);
|
||||||
|
delete clients[client.id];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
clients[client.id].active = true;
|
clients[client.id].active = true;
|
||||||
clients[client.id].shortName = data.shortName.toLowerCase();
|
clients[client.id].shortName = data.shortName.toLowerCase();
|
||||||
clients[client.id].filterCode = String(data.filterCode);
|
clients[client.id].filterCode = String(data.filterCode);
|
||||||
@@ -269,16 +287,35 @@ io.sockets.on('connection', function (client) {
|
|||||||
clients[client.id].filterType = String(data.filterType);
|
clients[client.id].filterType = String(data.filterType);
|
||||||
clients[client.id].talkgroupNums = [];
|
clients[client.id].talkgroupNums = [];
|
||||||
clients[client.id].timestamp = new Date();
|
clients[client.id].timestamp = new Date();
|
||||||
|
clients[client.id].firehose = false;
|
||||||
|
clients[client.id].ipAddress = ipAddress;
|
||||||
|
|
||||||
|
// Validate the filterCode and filterType
|
||||||
if (typeof clients[client.id].filterCode != "string") {
|
if (typeof clients[client.id].filterCode != "string") {
|
||||||
console.error("Error - Socket - Invalid filterCode: " + data.filterCode + " ShortName: " + data.shortName.toLowerCase());
|
console.error("Error - Socket - Invalid filterCode: " + data.filterCode + " ShortName: " + data.shortName.toLowerCase() + " ClientID: " + client.id + " IP: " + ipAddress);
|
||||||
delete clients[client.id];
|
delete clients[client.id];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((data.filterType == "group") && (typeof data.filterCode == "string") && (data.filterCode.indexOf(',') == -1)) {
|
if (typeof clients[client.id].filterType != "string") {
|
||||||
if (!ObjectId.isValid(data.filterCode)) {
|
console.error("Error - Socket - Invalid filterType: " + data.filterType + " ShortName: " + data.shortName.toLowerCase() + " ClientID: " + client.id + " IP: " + ipAddress);
|
||||||
console.error("Error - Socket - Invalid Group ID: " + data.filterCode);
|
delete clients[client.id];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the filterType is firehose and the filterCode is the firehose key
|
||||||
|
if (data.filterType == "firehose") {
|
||||||
|
if (firehose_key && (typeof data.filterCode == "string") && (data.filterCode == firehose_key)) {
|
||||||
|
console.log("Enabling Firehose mode for Client: " + client.id + " IP: " + ipAddress);
|
||||||
|
clients[client.id].firehose = true;
|
||||||
|
} else {
|
||||||
|
console.error("Error - Socket - Invalid Firehose Key: " + data.filterCode + " ClientID: " + client.id + " Filter Name: " + client.filterName + " IP: " + ipAddress);
|
||||||
|
delete clients[client.id];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if ((data.filterType == "group")) { // Handle filterType == "group"
|
||||||
|
if ((typeof data.filterCode != "string") || (data.filterCode.indexOf(',') != -1) || !ObjectId.isValid(data.filterCode)) {
|
||||||
|
console.error("Error - Socket - Invalid Group ID: " + data.filterCode + " ShortName: " + data.shortName.toLowerCase() + " ClientID: " + client.id + " IP: " + ipAddress);
|
||||||
delete clients[client.id];
|
delete clients[client.id];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -286,29 +323,32 @@ io.sockets.on('connection', function (client) {
|
|||||||
const group = await Group.findOne({ 'shortName': data.shortName.toLowerCase(), '_id': ObjectId.createFromHexString(data.filterCode) });
|
const group = await Group.findOne({ 'shortName': data.shortName.toLowerCase(), '_id': ObjectId.createFromHexString(data.filterCode) });
|
||||||
|
|
||||||
if (typeof clients[client.id] === "undefined") {
|
if (typeof clients[client.id] === "undefined") {
|
||||||
console.error("How can it be undefined here!!");
|
console.error("Error - Socket - Client not found ShortName: " + data.shortName + " ClientID: " + client.id + " IP: " + ipAddress);
|
||||||
console.error(client.id);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (group) {
|
if (group) {
|
||||||
clients[client.id].talkgroupNums = group.talkgroups;
|
clients[client.id].talkgroupNums = group.talkgroups;
|
||||||
} else {
|
} else {
|
||||||
console.error("Error - Socket: Invalid group " + data.filterCode + " Shortname: " + data.shortName + " ClientID: " + client.id);
|
console.error("Error - Socket - Invalid group " + data.filterCode + " Shortname: " + data.shortName + " ClientID: " + client.id + " IP: " + ipAddress);
|
||||||
delete clients[client.id];
|
delete clients[client.id];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Handle filterType == "talkgroup"
|
||||||
} else if ((data.filterType == "talkgroup") && Array.isArray(data.filterCode)) {
|
} else if ((data.filterType == "talkgroup") && Array.isArray(data.filterCode)) {
|
||||||
clients[client.id].talkgroupNums = data.filterCode;
|
clients[client.id].talkgroupNums = data.filterCode;
|
||||||
|
} else if ((data.filterType == "all") && (data.filterCode == "")) {
|
||||||
|
// Handle filterType == "all"
|
||||||
|
clients[client.id].talkgroupNums = [];
|
||||||
} else {
|
} else {
|
||||||
// This is for ALL filters
|
console.error("Error - Socket - Invalid Filter Type: " + data.filterType + " Filter Code: " + data.filterCode + " Filter Name: " + data.filterName + " ShortName: " + data.shortName.toLowerCase() + " ClientID: " + client.id + " IP: " + ipAddress);
|
||||||
//console.error("Invalid Socket Filter - Type: " + data.filterType + " Code: " + data.filterCode);
|
delete clients[client.id];
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
//console.log("[" + data.shortName.toLowerCase() + "] WebSocket Updating - Client: " + client.id + " code set to: " + data.filterCode + " type set to: " + data.filterType + " TGS: " + clients[client.id].talkgroupNums);
|
|
||||||
} else {
|
} else {
|
||||||
console.error("Error - Socket.io [Start] either client not found or no Short Name");
|
console.error("Error - Socket.io [Start] either client not found, shortName:" + data.shortName + " ClientID: " + client.id + " IP: " + ipAddress);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('stop', function (data) {
|
client.on('stop', function (data) {
|
||||||
if (clients[client.id]) {
|
if (clients[client.id]) {
|
||||||
clients[client.id].active = false;
|
clients[client.id].active = false;
|
||||||
@@ -324,15 +364,15 @@ io.sockets.on('connection', function (client) {
|
|||||||
stats.init_stats();
|
stats.init_stats();
|
||||||
// This will run at 3am each day.
|
// This will run at 3am each day.
|
||||||
// IF you don't set the minute to 0, it will run every minute while it is still 3, so at 3:01, 3:02... etc
|
// IF you don't set the minute to 0, it will run every minute while it is still 3, so at 3:01, 3:02... etc
|
||||||
schedule.scheduleJob('0 3 * * *', function() {
|
schedule.scheduleJob('0 3 * * *', function () {
|
||||||
db.cleanOldCalls();
|
db.cleanOldCalls();
|
||||||
});
|
});
|
||||||
|
|
||||||
schedule.scheduleJob('15 3 * * *', function() {
|
schedule.scheduleJob('15 3 * * *', function () {
|
||||||
db.cleanOldEvents();
|
db.cleanOldEvents();
|
||||||
});
|
});
|
||||||
|
|
||||||
schedule.scheduleJob('30 3 * * *', function() {
|
schedule.scheduleJob('30 3 * * *', function () {
|
||||||
db.cleanOldPodcasts();
|
db.cleanOldPodcasts();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
var mongoose = require('mongoose');
|
var mongoose = require('mongoose');
|
||||||
|
|
||||||
var srcSchema = mongoose.Schema({ pos: Number, src: String });
|
var srcSchema = mongoose.Schema({ pos: Number, src: String, tag: String });
|
||||||
|
|
||||||
const callSchema = mongoose.Schema({
|
const callSchema = mongoose.Schema({
|
||||||
talkgroupNum: Number,
|
talkgroupNum: Number,
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ services:
|
|||||||
- frontend
|
- frontend
|
||||||
- account
|
- account
|
||||||
- admin
|
- admin
|
||||||
command: /bin/bash -c "envsubst '$$DOMAIN_NAME' < /etc/nginx/conf.d/site.template > /etc/nginx/conf.d/default.conf && while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g 'daemon off;'"
|
|
||||||
|
|
||||||
certbot:
|
certbot:
|
||||||
image: certbot/certbot:latest
|
image: certbot/certbot:latest
|
||||||
@@ -73,6 +72,7 @@ services:
|
|||||||
- MONGO_PASSWORD
|
- MONGO_PASSWORD
|
||||||
- MAILJET_KEY
|
- MAILJET_KEY
|
||||||
- MAILJET_SECRET
|
- MAILJET_SECRET
|
||||||
|
- FIREHOSE_KEY
|
||||||
- REACT_APP_ADMIN_EMAIL
|
- REACT_APP_ADMIN_EMAIL
|
||||||
- REACT_APP_SITE_NAME
|
- REACT_APP_SITE_NAME
|
||||||
- NODE_ENV
|
- NODE_ENV
|
||||||
|
|||||||
@@ -107,7 +107,7 @@
|
|||||||
</p> <p><strong>Contact Us</strong></p> <p>
|
</p> <p><strong>Contact Us</strong></p> <p>
|
||||||
If you have any questions or suggestions about my
|
If you have any questions or suggestions about my
|
||||||
Terms and Conditions, do not hesitate to contact me
|
Terms and Conditions, do not hesitate to contact me
|
||||||
at luke@robotastic.com.
|
at support@openmhz.com.
|
||||||
</p> <p>This Terms and Conditions page was generated by <a href="https://app-privacy-policy-generator.nisrulz.com/" target="_blank" rel="noopener noreferrer">App Privacy Policy Generator</a></p>
|
</p> <p>This Terms and Conditions page was generated by <a href="https://app-privacy-policy-generator.nisrulz.com/" target="_blank" rel="noopener noreferrer">App Privacy Policy Generator</a></p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
this page.
|
this page.
|
||||||
</p> <p>This policy is effective as of 2023-09-16</p> <p><strong>Contact Us</strong></p> <p>
|
</p> <p>This policy is effective as of 2023-09-16</p> <p><strong>Contact Us</strong></p> <p>
|
||||||
If you have any questions or suggestions about my
|
If you have any questions or suggestions about my
|
||||||
Privacy Policy, do not hesitate to contact me at luke@robotastic.com.
|
Privacy Policy, do not hesitate to contact me at support@openmhz.com.
|
||||||
</p> <p>This privacy policy page was created at <a href="https://privacypolicytemplate.net" target="_blank" rel="noopener noreferrer">privacypolicytemplate.net </a>and modified/generated by <a href="https://app-privacy-policy-generator.nisrulz.com/" target="_blank" rel="noopener noreferrer">App Privacy Policy Generator</a></p>
|
</p> <p>This privacy policy page was created at <a href="https://privacypolicytemplate.net" target="_blank" rel="noopener noreferrer">privacypolicytemplate.net </a>and modified/generated by <a href="https://app-privacy-policy-generator.nisrulz.com/" target="_blank" rel="noopener noreferrer">App Privacy Policy Generator</a></p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ var favicon = require('serve-favicon')
|
|||||||
const secrets = require("./secrets");
|
const secrets = require("./secrets");
|
||||||
|
|
||||||
|
|
||||||
var cookie_domain = process.env['REACT_APP_COOKIE_DOMAIN'] != null ? process.env['REACT_APP_COOKIE_DOMAIN'] : '.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
var cookie_domain = process.env['REACT_APP_COOKIE_DOMAIN'] != null ? process.env['REACT_APP_COOKIE_DOMAIN'] : '.openmhz.com';
|
||||||
var backend_server = process.env['REACT_APP_BACKEND_SERVER'] != null ? process.env['REACT_APP_BACKEND_SERVER'] : 'https://api.openmhz.com';
|
var backend_server = process.env['REACT_APP_BACKEND_SERVER'] != null ? process.env['REACT_APP_BACKEND_SERVER'] : 'https://api.openmhz.com';
|
||||||
var frontend_server = process.env['REACT_APP_FRONTEND_SERVER'] != null ? process.env['REACT_APP_FRONTEND_SERVER'] : 'https://openmhz.com';
|
var frontend_server = process.env['REACT_APP_FRONTEND_SERVER'] != null ? process.env['REACT_APP_FRONTEND_SERVER'] : 'https://openmhz.com';
|
||||||
var account_server = process.env['REACT_APP_ACCOUNT_SERVER'] != null ? process.env['REACT_APP_ACCOUNT_SERVER'] : 'https://account.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
var account_server = process.env['REACT_APP_ACCOUNT_SERVER'] != null ? process.env['REACT_APP_ACCOUNT_SERVER'] : 'https://account.openmhz.com';
|
||||||
var dev_server = frontend_server + ":3000"
|
var dev_server = frontend_server + ":3000"
|
||||||
|
|
||||||
module.exports = function(app) {
|
module.exports = function(app) {
|
||||||
|
|||||||
@@ -70,136 +70,6 @@ function getCard(req, res, next) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
exports.get_card = function(req, res) {
|
|
||||||
var objectId = req.params.id;
|
|
||||||
try {
|
|
||||||
var o_id = ObjectID.createFromHexString(objectId);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("Error - /card/:id generating ObjectID " + err);
|
|
||||||
res.status(500);
|
|
||||||
res.send(JSON.stringify({
|
|
||||||
error: err,
|
|
||||||
"_id": objectId
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
db.get().collection('calls', function(err, transCollection) {
|
|
||||||
transCollection.findOne({
|
|
||||||
'_id': o_id
|
|
||||||
},
|
|
||||||
function(err, item) {
|
|
||||||
//console.log(util.inspect(item));
|
|
||||||
if (item) {
|
|
||||||
var time = new Date(item.time);
|
|
||||||
var timeString = time.toLocaleTimeString("en-US");
|
|
||||||
var dateString = time.toDateString();
|
|
||||||
res.render('card', {
|
|
||||||
item: item,
|
|
||||||
channel: channels[item.talkgroupNum],
|
|
||||||
time: timeString,
|
|
||||||
date: dateString
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.warn("Error - /card/:id Could not find Item " + err);
|
|
||||||
res.send(404, 'Sorry, we cannot find that!');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
async function getPodcast(req, res, next) {
|
|
||||||
const now = new Date().toLocaleString();
|
|
||||||
|
|
||||||
const feedStart = `
|
|
||||||
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:psc="http://podlove.org/simple-chapters" xmlns:podcast="https://podcastindex.org/namespace/1.0" version="2.0">
|
|
||||||
<channel>
|
|
||||||
<title>
|
|
||||||
<![CDATA[ OpenMHz ]]>
|
|
||||||
</title>
|
|
||||||
<description>
|
|
||||||
<![CDATA[ Real public safety radio recordings, from across the country. <a href="hhtps://openmhz.com">OpenMHz</a> is a website that makes it easy to share recordings of public safety radio system. This podcast is a collection of interesting events that has been curated by the OpenMHz community. ]]>
|
|
||||||
</description>
|
|
||||||
<link>https://openmhz.com</link>
|
|
||||||
<generator>Podcast for Node</generator>
|
|
||||||
<lastBuildDate>${now}</lastBuildDate>
|
|
||||||
<atom:link href="https://openmhz.com/rss.xml" rel="self" type="application/rss+xml"/>
|
|
||||||
<author>
|
|
||||||
<![CDATA[ OpenMHz ]]>
|
|
||||||
</author>
|
|
||||||
<pubDate>${now}</pubDate>
|
|
||||||
<copyright>
|
|
||||||
<![CDATA[ © 2023 Robotastic ]]>
|
|
||||||
</copyright>
|
|
||||||
<language>
|
|
||||||
<![CDATA[ en ]]>
|
|
||||||
</language>
|
|
||||||
<ttl>60</ttl>
|
|
||||||
<itunes:author>OpenMHz</itunes:author>
|
|
||||||
<itunes:subtitle>I am a sub title</itunes:subtitle>
|
|
||||||
<itunes:summary>Real public safety radio recordings, from across the country. OpenMHz is a website that makes it easy to share recordings of public safety radio system. This podcast is a collection of interesting events that has been curated by the OpenMHz community.</itunes:summary>
|
|
||||||
<itunes:owner>
|
|
||||||
<itunes:name>Luke Berndt</itunes:name>
|
|
||||||
<itunes:email>luke@robotastic.com</itunes:email>
|
|
||||||
</itunes:owner>
|
|
||||||
<itunes:explicit>false</itunes:explicit>
|
|
||||||
<itunes:category text="Government"/>
|
|
||||||
<itunes:category text="Daily News"/>
|
|
||||||
<itunes:category text="True Crime"/>
|
|
||||||
<itunes:image href="https://openmhz.com/podcast/cover.png"/>
|
|
||||||
`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const feedEnd = `
|
|
||||||
</channel>
|
|
||||||
</rss>
|
|
||||||
`
|
|
||||||
let xml = feedStart;
|
|
||||||
try {
|
|
||||||
const podcastsCollection = db.get().collection('podcasts');
|
|
||||||
podcastsCollection.find().toArray(function (err, podcasts) {
|
|
||||||
|
|
||||||
|
|
||||||
for (const podcast of podcasts) {
|
|
||||||
let description = podcast.description + "\n\nRadio Systems: \n";
|
|
||||||
for (const system of podcast.systems) {
|
|
||||||
description = description + " - " + system + "\n";
|
|
||||||
}
|
|
||||||
xml = xml + `
|
|
||||||
<item>
|
|
||||||
<title>
|
|
||||||
<![CDATA[ ${podcast.title} ]]>
|
|
||||||
</title>
|
|
||||||
<description>
|
|
||||||
<![CDATA[ ${description} ]]>
|
|
||||||
</description>
|
|
||||||
<guid isPermaLink="false">${podcast.downloadUrl}</guid>
|
|
||||||
<dc:creator>
|
|
||||||
<![CDATA[ OpenMHz ]]>
|
|
||||||
</dc:creator>
|
|
||||||
<url>${podcast.eventUrl}</url>
|
|
||||||
<pubDate>${podcast.startTime}</pubDate>
|
|
||||||
<enclosure url="${podcast.downloadUrl}" length="0" type="audio/mp4"/>
|
|
||||||
<itunes:author>OpenMHz</itunes:author>
|
|
||||||
<itunes:summary>${description}</itunes:summary>
|
|
||||||
<itunes:explicit>false</itunes:explicit>
|
|
||||||
</item>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
xml = xml + feedEnd;
|
|
||||||
res.set('Content-Type', 'text/xml');
|
|
||||||
res.send(xml);
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
const dateRange = (start, end) => {
|
const dateRange = (start, end) => {
|
||||||
if (start.getYear() != end.getYear()) {
|
if (start.getYear() != end.getYear()) {
|
||||||
return { "part1": start.toLocaleDateString('en-us', { weekday: "short", year: "numeric", month: "short", day: "numeric" }) + " " + start.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }), "part2": end.toLocaleDateString('en-us', { weekday: "short", year: "numeric", month: "short", day: "numeric" }) + " " + end.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) };
|
return { "part1": start.toLocaleDateString('en-us', { weekday: "short", year: "numeric", month: "short", day: "numeric" }) + " " + start.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }), "part2": end.toLocaleDateString('en-us', { weekday: "short", year: "numeric", month: "short", day: "numeric" }) + " " + end.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) };
|
||||||
@@ -227,14 +97,14 @@ async function getPodcast(req, res, next) {
|
|||||||
siteUrl: 'https://openmhz.com',
|
siteUrl: 'https://openmhz.com',
|
||||||
imageUrl: 'https://openmhz.com/podcast/cover.png',
|
imageUrl: 'https://openmhz.com/podcast/cover.png',
|
||||||
author: 'OpenMHz',
|
author: 'OpenMHz',
|
||||||
copyright: '© 2022 Robotastic',
|
copyright: '© 2026 Robotastic',
|
||||||
language: 'en',
|
language: 'en',
|
||||||
pubDate: 'May 20, 2012 04:00:00 GMT',
|
pubDate: 'May 20, 2012 04:00:00 GMT',
|
||||||
ttl: 60,
|
ttl: 60,
|
||||||
itunesAuthor: 'OpenMHz',
|
itunesAuthor: 'OpenMHz',
|
||||||
itunesSubtitle: 'I am a sub title',
|
itunesSubtitle: 'I am a sub title',
|
||||||
itunesSummary: 'Real public safety radio recordings, from across the country. OpenMHz is a website that makes it easy to share recordings of public safety radio system. This podcast is a collection of interesting events that has been curated by the OpenMHz community.',
|
itunesSummary: 'Real public safety radio recordings, from across the country. OpenMHz is a website that makes it easy to share recordings of public safety radio system. This podcast is a collection of interesting events that has been curated by the OpenMHz community.',
|
||||||
itunesOwner: { name: 'Luke Berndt', email: 'luke@robotastic.com' },
|
itunesOwner: { name: 'Luke Berndt', email: 'support@openmhz.com' },
|
||||||
itunesExplicit: false,
|
itunesExplicit: false,
|
||||||
itunesCategory: [{
|
itunesCategory: [{
|
||||||
text: 'Government'
|
text: 'Government'
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const AboutComponent = () => {
|
|||||||
<Container text>
|
<Container text>
|
||||||
<Divider horizontal style={{ paddingTop: "4em", paddingBottom: "2em" }}><Header as="h1">About</Header></Divider>
|
<Divider horizontal style={{ paddingTop: "4em", paddingBottom: "2em" }}><Header as="h1">About</Header></Divider>
|
||||||
|
|
||||||
<p>I wrote <a href="https://github.com/robotastic/trunk-recorder">Trunk Recorder</a> because I was curious
|
<p>I wrote <a href="https://github.com/TrunkRecorder/trunk-recorder">Trunk Recorder</a> because I was curious
|
||||||
about what my local fire station was up to and I put together the original version of OpenMHz because I
|
about what my local fire station was up to and I put together the original version of OpenMHz because I
|
||||||
figured other people might want to listen to the recordings too.</p>
|
figured other people might want to listen to the recordings too.</p>
|
||||||
<p>The latest version of this site makes it easy for other people running Trunk Recorder to share their recordings.
|
<p>The latest version of this site makes it easy for other people running Trunk Recorder to share their recordings.
|
||||||
@@ -30,13 +30,7 @@ const AboutComponent = () => {
|
|||||||
<Divider horizontal style={{ paddingTop: "4em", paddingBottom: "2em" }}><Header as="h2">100% Open Source</Header></Divider>
|
<Divider horizontal style={{ paddingTop: "4em", paddingBottom: "2em" }}><Header as="h2">100% Open Source</Header></Divider>
|
||||||
<p>Have changes you would like to suggest? Want to run your own server for your community? All of the code for OpenMHz is available as open source:</p>
|
<p>Have changes you would like to suggest? Want to run your own server for your community? All of the code for OpenMHz is available as open source:</p>
|
||||||
<p><a href="https://github.com/openmhz/trunk-server"><Icon name='github' />Trunk Server</a></p>
|
<p><a href="https://github.com/openmhz/trunk-server"><Icon name='github' />Trunk Server</a></p>
|
||||||
<Divider horizontal style={{ paddingTop: "4em", paddingBottom: "2em" }}><Header as="h2">Support</Header></Divider>
|
|
||||||
<p>If OpenMHz brings you joy, think about becoming a supporter! It will cover hosting costs and help keep me focused on development.</p>
|
|
||||||
|
|
||||||
<p><a href="https://github.com/sponsors/robotastic"><Button color='green'>
|
|
||||||
<Icon name='heart' />
|
|
||||||
Support
|
|
||||||
</Button></a></p>
|
|
||||||
|
|
||||||
<Divider horizontal style={{ paddingTop: "4em", paddingBottom: "2em" }}><Header as="h2">FAQ</Header></Divider>
|
<Divider horizontal style={{ paddingTop: "4em", paddingBottom: "2em" }}><Header as="h2">FAQ</Header></Divider>
|
||||||
<List>
|
<List>
|
||||||
@@ -45,7 +39,7 @@ const AboutComponent = () => {
|
|||||||
<ListContent>
|
<ListContent>
|
||||||
<ListHeader as="h2">Where do the different feeds come from</ListHeader>
|
<ListHeader as="h2">Where do the different feeds come from</ListHeader>
|
||||||
<ListDescription>
|
<ListDescription>
|
||||||
It is a team effort! There are contributors from around country and even internationally, that are running <a href="https://github.com/robotastic/trunk-recorder">Trunk Recorder</a> to capture local radio systems and sending the transmissions to OpenMHz.
|
It is a team effort! There are contributors from around country and even internationally, that are running <a href="https://github.com/TrunkRecorder/trunk-recorder">Trunk Recorder</a> to capture local radio systems and sending the transmissions to OpenMHz.
|
||||||
</ListDescription>
|
</ListDescription>
|
||||||
</ListContent>
|
</ListContent>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@@ -98,7 +92,7 @@ const AboutComponent = () => {
|
|||||||
<ListHeader as="h2">I have other questions</ListHeader>
|
<ListHeader as="h2">I have other questions</ListHeader>
|
||||||
<ListDescription>
|
<ListDescription>
|
||||||
<p>
|
<p>
|
||||||
Check out the <Link to="/terms">Terms of Service</Link>, there might be an answer there or send me an email at <a href="mailto:luke@robotastic.com?Subject=OpenMHz" target="_top">luke@robotastic.com</a>
|
Check out the <Link to="/terms">Terms of Service</Link>, there might be an answer there or send me an email at <a href="mailto:support@openmhz.com?Subject=OpenMHz" target="_top">support@openmhz.com</a>
|
||||||
</p>
|
</p>
|
||||||
</ListDescription>
|
</ListDescription>
|
||||||
</ListContent>
|
</ListContent>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ You agree not to sell, resell, or offer for any commercial purposes, any portion
|
|||||||
|
|
||||||
<Header as='h2'>5. API Access</Header>
|
<Header as='h2'>5. API Access</Header>
|
||||||
|
|
||||||
The OpenMHz API is designed solely to support the OpenMHz website frontend and iOS app. You may not use the API for any other purpose without explicit permission from OpenMHz. The infrastructure for OpenMHz has not been well designed, and unauthorized use may result in the whole thing crashing. Additionally, some API calls may place a significant load on the server, and preplanning is required to avoid this. We will allow for API access on a case-by-case basis. Please contact us at: luke@robotastic.com
|
The OpenMHz API is designed solely to support the OpenMHz website frontend and iOS app. You may not use the API for any other purpose without explicit permission from OpenMHz. The infrastructure for OpenMHz has not been well designed, and unauthorized use may result in the whole thing crashing. Additionally, some API calls may place a significant load on the server, and preplanning is required to avoid this. We will allow for API access on a case-by-case basis. Please contact us at: support@openmhz.com
|
||||||
|
|
||||||
<Header as='h2'>6. Legal Compliance</Header>
|
<Header as='h2'>6. Legal Compliance</Header>
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ The Services are provided as a labor of love by a single individual. I have a li
|
|||||||
|
|
||||||
<Header as='h2'>14. Contact Me</Header>
|
<Header as='h2'>14. Contact Me</Header>
|
||||||
|
|
||||||
If you have any questions or concerns about these Terms, please contact me at luke@robotastic.com.
|
If you have any questions or concerns about these Terms, please contact me at support@openmhz.com.
|
||||||
|
|
||||||
By using OpenMHz, you acknowledge that you have read, understood, and agree to be bound by these Terms of Service. Thank you for using our Services!
|
By using OpenMHz, you acknowledge that you have read, understood, and agree to be bound by these Terms of Service. Thank you for using our Services!
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ function CallInfo(props) {
|
|||||||
callFreq = Math.round(freq * 1000) / 1000;
|
callFreq = Math.round(freq * 1000) / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
srcList = currentCall.srcList.map((source, index) => <List.Item key={index}>{source.src}[{source.pos}]</List.Item>);
|
srcList = currentCall.srcList.map((source, index) => <List.Item key={index}>{source.src}[{source.pos}] {source.tag ? "(" + source.tag + ")" : ""}</List.Item>);
|
||||||
callLength = currentCall.len;
|
callLength = currentCall.len;
|
||||||
talkgroupNum = currentCall.talkgroupNum;
|
talkgroupNum = currentCall.talkgroupNum;
|
||||||
callDownload = currentCall.url;
|
callDownload = currentCall.url;
|
||||||
|
|||||||
@@ -177,6 +177,19 @@ function CallPlayer(props) {
|
|||||||
}
|
}
|
||||||
}, [system])
|
}, [system])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const requestWakeLock = async () => {
|
||||||
|
try {
|
||||||
|
const wakeLock = await navigator.wakeLock.request("screen");
|
||||||
|
} catch (err) {
|
||||||
|
// the wake lock request fails - usually system related, such being low on battery
|
||||||
|
console.log(`${err.name}, ${err.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
requestWakeLock();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleStatusDismiss = () => {
|
const handleStatusDismiss = () => {
|
||||||
setStatusVisible(false);
|
setStatusVisible(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function CallInfo(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
srcList = currentCall.srcList.map((source, index) => <List.Item key={index}>{source.src}[{source.pos}]</List.Item>);
|
srcList = currentCall.srcList.map((source, index) => <List.Item key={index}>{source.src}[{source.pos}] {source.tag ? "(" + source.tag + ")" : ""}</List.Item>);
|
||||||
callLength = currentCall.len;
|
callLength = currentCall.len;
|
||||||
talkgroupNum = currentCall.talkgroupNum;
|
talkgroupNum = currentCall.talkgroupNum;
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ function CallInfoPane(props) {
|
|||||||
let patchString = "";
|
let patchString = "";
|
||||||
let header = "Call Info"
|
let header = "Call Info"
|
||||||
let title = ""
|
let title = ""
|
||||||
|
let callEmergnecy = (<> </>);
|
||||||
const currentCall = props.call ? props.call : false;
|
const currentCall = props.call ? props.call : false;
|
||||||
const { callLink, callDownload, callTweet } = useCallLink(props.call)
|
const { callLink, callDownload, callTweet } = useCallLink(props.call)
|
||||||
const { data: allSystems, isSuccess } = useGetSystemsQuery();
|
const { data: allSystems, isSuccess } = useGetSystemsQuery();
|
||||||
@@ -50,12 +51,16 @@ function CallInfoPane(props) {
|
|||||||
const freq = currentCall.freq / 1000000;
|
const freq = currentCall.freq / 1000000;
|
||||||
callFreq = Math.round(freq * 1000) / 1000;
|
callFreq = Math.round(freq * 1000) / 1000;
|
||||||
}
|
}
|
||||||
srcList = currentCall.srcList.map((source, index) => <List.Item key={index}>{source.src}[{source.pos}]</List.Item>);
|
srcList = currentCall.srcList.map((source, index) => <List.Item key={index}>{source.src}[{source.pos}] {source.tag ? "(" + source.tag + ")" : ""}</List.Item>);
|
||||||
callLength = currentCall.len;
|
callLength = currentCall.len;
|
||||||
talkgroupNum = currentCall.talkgroupNum;
|
talkgroupNum = currentCall.talkgroupNum;
|
||||||
patches = currentCall.patches;
|
patches = currentCall.patches;
|
||||||
|
|
||||||
if(patches.length > 1) {
|
if (currentCall.emergency) {
|
||||||
|
callEmergnecy = (<div><Icon name="warning sign" color="red" /> Emergency </div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(patches && (patches.length > 1)) {
|
||||||
patchString = patches.join(", ");
|
patchString = patches.join(", ");
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
@@ -100,6 +105,7 @@ function CallInfoPane(props) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header as='h1'>{header}</Header>
|
<Header as='h1'>{header}</Header>
|
||||||
|
{callEmergnecy}
|
||||||
<List bulleted horizontal link>
|
<List bulleted horizontal link>
|
||||||
{srcList}
|
{srcList}
|
||||||
</List>
|
</List>
|
||||||
|
|||||||
@@ -105,6 +105,14 @@ const CallItem = (props) => {
|
|||||||
key: "blue",
|
key: "blue",
|
||||||
inverted: "true"
|
inverted: "true"
|
||||||
}
|
}
|
||||||
|
} else if (call.emergency) {
|
||||||
|
rowSelected = {
|
||||||
|
positive: true,
|
||||||
|
error: true,
|
||||||
|
color: "red",
|
||||||
|
key: "red",
|
||||||
|
inverted: "true"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let talkgroup;
|
let talkgroup;
|
||||||
if ((typeof talkgroups== 'undefined') || (typeof talkgroups.talkgroups[call.talkgroupNum] == 'undefined')) {
|
if ((typeof talkgroups== 'undefined') || (typeof talkgroups.talkgroups[call.talkgroupNum] == 'undefined')) {
|
||||||
|
|||||||
@@ -21,11 +21,16 @@
|
|||||||
|
|
||||||
.label-item {
|
.label-item {
|
||||||
padding: 9px 2px 5px 2px;
|
padding: 9px 2px 5px 2px;
|
||||||
width: 130px;
|
width: 150px;
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.tag-label {
|
||||||
|
max-width: 75px;
|
||||||
|
overflow: clip;
|
||||||
|
text-wrap-mode: nowrap;
|
||||||
|
}
|
||||||
.mediaplayer {
|
.mediaplayer {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
color: white;
|
color: white;
|
||||||
|
|||||||
@@ -136,10 +136,14 @@ const MediaPlayer = (props) => {
|
|||||||
|
|
||||||
let playEnabled = { "disabled": true }
|
let playEnabled = { "disabled": true }
|
||||||
let sourceId = "-";
|
let sourceId = "-";
|
||||||
|
let sourceTag = "";
|
||||||
|
|
||||||
if (call) {
|
if (call) {
|
||||||
if (call.srcList.length > sourceIndex) {
|
if (call.srcList.length > sourceIndex) {
|
||||||
sourceId = call.srcList[sourceIndex].src;
|
sourceId = call.srcList[sourceIndex].src;
|
||||||
|
if (call.srcList[sourceIndex].tag !== undefined && call.srcList[sourceIndex].tag !== null && call.srcList[sourceIndex].tag !== "") {
|
||||||
|
sourceTag = call.srcList[sourceIndex].tag;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
playEnabled = {};
|
playEnabled = {};
|
||||||
}
|
}
|
||||||
@@ -200,8 +204,8 @@ const MediaPlayer = (props) => {
|
|||||||
{playTime}
|
{playTime}
|
||||||
Sec
|
Sec
|
||||||
</Label>
|
</Label>
|
||||||
<Label color="black" className="desktop-only">
|
<Label color="black" className="desktop-only tag-label">
|
||||||
{sourceId}
|
{sourceTag!== "" ? sourceTag : sourceId}
|
||||||
</Label>
|
</Label>
|
||||||
</LabelGroup>
|
</LabelGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ function PlaylistBuilder(props) {
|
|||||||
<Modal.Description>
|
<Modal.Description>
|
||||||
<Message icon>
|
<Message icon>
|
||||||
<Icon name='warning sign' />
|
<Icon name='warning sign' />
|
||||||
<Message.Content>The Events feature is a work in progress! Things may break, change, or get deleted. Contact me with ideas or problems: luke@robotastic.com</Message.Content>
|
<Message.Content>The Events feature is a work in progress! Things may break, change, or get deleted. Contact me with ideas or problems: support@openmhz.com</Message.Content>
|
||||||
</Message>
|
</Message>
|
||||||
<Header>Event</Header>
|
<Header>Event</Header>
|
||||||
<Form>
|
<Form>
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ function SupportModal(props) {
|
|||||||
<p>If OpenMHz brings you joy, think about becoming a supporter! It will cover hosting costs and help keep me focused on development.</p>
|
<p>If OpenMHz brings you joy, think about becoming a supporter! It will cover hosting costs and help keep me focused on development.</p>
|
||||||
|
|
||||||
<Divider horizontal>Donate</Divider>
|
<Divider horizontal>Donate</Divider>
|
||||||
<a href="https://github.com/sponsors/robotastic"><Button ><Icon name="github"/>GitHub Sponsors</Button></a>
|
|
||||||
|
|
||||||
<a href="https://patreon.com/OpenMHz"><Button ><Icon name="patreon"/>Patreon </Button></a>
|
<a href="https://patreon.com/OpenMHz"><Button ><Icon name="patreon"/>Patreon </Button></a>
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const ListEventCalls = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<Table id="calls" unstackable >
|
<Table id="calls" unstackable style={{paddingBottom: '75px'}} >
|
||||||
<Table.Header >
|
<Table.Header >
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<Table.HeaderCell> </Table.HeaderCell>
|
<Table.HeaderCell> </Table.HeaderCell>
|
||||||
|
|||||||
@@ -650,7 +650,7 @@ const Main = (props) => {
|
|||||||
Join us! Record a local radio system and share it with the world. If you have a spare computer, it is easy to get started. With a $25 SDR or two, you can capture an entire system.
|
Join us! Record a local radio system and share it with the world. If you have a spare computer, it is easy to get started. With a $25 SDR or two, you can capture an entire system.
|
||||||
It does take a little bit of work to get things setup, but we are here to help and answer questions.
|
It does take a little bit of work to get things setup, but we are here to help and answer questions.
|
||||||
</p>
|
</p>
|
||||||
<a href="https://github.com/robotastic/trunk-recorder/blob/master/README.md">
|
<a href="https://github.com/TrunkRecorder/trunk-recorder/blob/master/README.md">
|
||||||
<Button size='large'>
|
<Button size='large'>
|
||||||
Learn More
|
Learn More
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ function ContactModal(props) {
|
|||||||
</List>
|
</List>
|
||||||
</p>
|
</p>
|
||||||
<p>If you have general questions about OpenMHz send me an email, </p>
|
<p>If you have general questions about OpenMHz send me an email, </p>
|
||||||
<p> - Luke <a href="mailto:luke@robotastic.com?Subject=OpenMHz" target="_top">luke@robotastic.com</a> </p>
|
<p> - Luke <a href="mailto:support@openmhz.com?Subject=OpenMHz" target="_top">support@openmhz.com</a> </p>
|
||||||
|
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
<Grid.Column width={10}>
|
<Grid.Column width={10}>
|
||||||
|
|||||||
@@ -24,3 +24,7 @@ COPY *.conf /etc/nginx/
|
|||||||
|
|
||||||
# Add virtual hosts
|
# Add virtual hosts
|
||||||
COPY ${STAGE}/vhosts/ /etc/nginx/conf.d/
|
COPY ${STAGE}/vhosts/ /etc/nginx/conf.d/
|
||||||
|
|
||||||
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
|
|||||||
20
nginx-proxy/entrypoint.sh
Normal file
20
nginx-proxy/entrypoint.sh
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Render the config from template
|
||||||
|
envsubst '$DOMAIN_NAME' < /etc/nginx/conf.d/site.template > /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# Test Nginx config
|
||||||
|
nginx -t
|
||||||
|
|
||||||
|
# Background job: reload Nginx every 6 hours to pick up new certs
|
||||||
|
(
|
||||||
|
while true; do
|
||||||
|
sleep 6h
|
||||||
|
echo "[nginx] Reloading config..."
|
||||||
|
nginx -s reload && echo "[nginx] Reload succeeded" || echo "[nginx] Reload failed"
|
||||||
|
done
|
||||||
|
) &
|
||||||
|
|
||||||
|
# Start Nginx in the foreground
|
||||||
|
exec nginx -g 'daemon off;'
|
||||||
@@ -1,5 +1,18 @@
|
|||||||
limit_req_zone $binary_remote_addr zone=perip:10m rate=1r/s;
|
limit_req_zone $binary_remote_addr zone=perip:10m rate=1r/s;
|
||||||
|
|
||||||
|
# Only send "Connection: upgrade" for real WebSocket requests, so ordinary
|
||||||
|
# requests can reuse the upstream keepalive pool.
|
||||||
|
map $http_upgrade $backend_connection {
|
||||||
|
default upgrade;
|
||||||
|
'' '';
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream backend_api {
|
||||||
|
server backend:3005;
|
||||||
|
keepalive 64;
|
||||||
|
keepalive_timeout 60s;
|
||||||
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name account.${DOMAIN_NAME} api.${DOMAIN_NAME} admin.${DOMAIN_NAME} www.${DOMAIN_NAME} ${DOMAIN_NAME};
|
server_name account.${DOMAIN_NAME} api.${DOMAIN_NAME} admin.${DOMAIN_NAME} www.${DOMAIN_NAME} ${DOMAIN_NAME};
|
||||||
@@ -87,10 +100,10 @@ server {
|
|||||||
|
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://backend:3005;
|
proxy_pass http://backend_api;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection $backend_connection;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_cache_bypass $http_upgrade;
|
proxy_cache_bypass $http_upgrade;
|
||||||
include /etc/nginx/proxy.conf;
|
include /etc/nginx/proxy.conf;
|
||||||
|
|||||||
@@ -1,4 +1,17 @@
|
|||||||
|
|
||||||
|
# Only send "Connection: upgrade" for real WebSocket requests, so ordinary
|
||||||
|
# requests can reuse the upstream keepalive pool.
|
||||||
|
map $http_upgrade $backend_connection {
|
||||||
|
default upgrade;
|
||||||
|
'' '';
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream backend_api {
|
||||||
|
server backend:3005;
|
||||||
|
keepalive 64;
|
||||||
|
keepalive_timeout 60s;
|
||||||
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
|
|
||||||
@@ -89,11 +102,11 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://backend:3005;
|
proxy_pass http://backend_api;
|
||||||
include /etc/nginx/proxy.conf;
|
include /etc/nginx/proxy.conf;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection $backend_connection;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_cache_bypass $http_upgrade;
|
proxy_cache_bypass $http_upgrade;
|
||||||
access_log /dev/stdout main;
|
access_log /dev/stdout main;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ export MAILJET_SECRET="FILL IN HERE"
|
|||||||
export REACT_APP_SITE_NAME=OpenMHz
|
export REACT_APP_SITE_NAME=OpenMHz
|
||||||
export REACT_APP_ADMIN_EMAIL="admin@email.com"
|
export REACT_APP_ADMIN_EMAIL="admin@email.com"
|
||||||
export DOMAIN_NAME="openmhz.com"
|
export DOMAIN_NAME="openmhz.com"
|
||||||
|
export FIREHOSE_KEY="firehose_key"
|
||||||
export S3_PROFILE='default'
|
export S3_PROFILE='default'
|
||||||
export S3_REGION='us-west-001'
|
export S3_REGION='us-west-001'
|
||||||
export S3_ENDPOINT='https://s3.us-west-1.wasabisys.com'
|
export S3_ENDPOINT='https://s3.us-west-1.wasabisys.com'
|
||||||
|
|||||||
1
utils/websocket_example/.gitignore
vendored
Normal file
1
utils/websocket_example/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
||||||
59
utils/websocket_example/index.js
Normal file
59
utils/websocket_example/index.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// Install first: npm install socket.io-client
|
||||||
|
const io = require('socket.io-client');
|
||||||
|
const serverUrl = 'http://api.openmhz.test'; // Update URL to match your server
|
||||||
|
|
||||||
|
// Connect to the WebSocket server
|
||||||
|
const socket = io(serverUrl); // Update URL to match your server
|
||||||
|
|
||||||
|
|
||||||
|
// filterType can be "group", "talkgroup", "all", or "firehose"
|
||||||
|
// if filterType is "group" then filterCode is the group ID
|
||||||
|
// if filterType is "talkgroup" then filterCode is the talkgroup ID
|
||||||
|
// if filterType is "all" then filterCode is ""
|
||||||
|
// if filterType is "firehose" then filterCode is set to the FIREHOSE_KEY
|
||||||
|
|
||||||
|
// shortName is the shortName for the System to monitor, unless it is in firehose mode, then it is set to ""
|
||||||
|
// filterStarred is ignored, so just set to false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Connection event handlers
|
||||||
|
socket.on('connect', () => {
|
||||||
|
console.log('Connected to server');
|
||||||
|
socket.emit("start", {
|
||||||
|
filterCode: "",
|
||||||
|
filterType: "all",
|
||||||
|
filterName: "Test Client",
|
||||||
|
filterStarred: false,
|
||||||
|
shortName: "test"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
console.log('Disconnected from server');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for incoming messages
|
||||||
|
socket.on('new message', (message) => {
|
||||||
|
message = JSON.parse(message);
|
||||||
|
const date = new Date(message.time);
|
||||||
|
console.log('\n=== Call Details ===');
|
||||||
|
console.log(`Time: ${date.toLocaleString()}`);
|
||||||
|
console.log(`Talkgroup: ${message.talkgroupNum}`);
|
||||||
|
console.log(`System: ${message.shortName}`);
|
||||||
|
console.log(`Duration: ${message.len} seconds`);
|
||||||
|
console.log(`Frequency: ${(message.freq / 1000000).toFixed(3)} MHz`);
|
||||||
|
console.log(`Emergency: ${message.emergency ? 'Yes' : 'No'}`);
|
||||||
|
console.log('\nSource List:');
|
||||||
|
message.srcList.forEach(src => {
|
||||||
|
console.log(`- Source ${src.src} at position ${src.pos}s`);
|
||||||
|
});
|
||||||
|
console.log('\nAudio URL:', message.url);
|
||||||
|
console.log('==================\n');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handling
|
||||||
|
socket.on('connect_error', (error) => {
|
||||||
|
console.error('Connection error:', error);
|
||||||
|
});
|
||||||
|
|
||||||
116
utils/websocket_example/package-lock.json
generated
Normal file
116
utils/websocket_example/package-lock.json
generated
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
{
|
||||||
|
"name": "websocket_example",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "websocket_example",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"socket.io-client": "^4.8.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@socket.io/component-emitter": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-client": {
|
||||||
|
"version": "6.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.2.tgz",
|
||||||
|
"integrity": "sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1",
|
||||||
|
"engine.io-parser": "~5.2.1",
|
||||||
|
"ws": "~8.17.1",
|
||||||
|
"xmlhttprequest-ssl": "~2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-parser": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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=="
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-client": {
|
||||||
|
"version": "4.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
||||||
|
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.2",
|
||||||
|
"engine.io-client": "~6.6.1",
|
||||||
|
"socket.io-parser": "~4.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser": {
|
||||||
|
"version": "4.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||||
|
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlhttprequest-ssl": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
utils/websocket_example/package.json
Normal file
14
utils/websocket_example/package.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "websocket_example",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "Luke Berndt",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"socket.io-client": "^4.8.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user