diff --git a/.env.test b/.env.test index 9321907..2b95e8b 100644 --- a/.env.test +++ b/.env.test @@ -14,7 +14,7 @@ REACT_APP_FRONTEND_SERVER="${PROTOCOL}${DOMAIN_NAME}" REACT_APP_ACCOUNT_SERVER="${PROTOCOL}account.${DOMAIN_NAME}" REACT_APP_ADMIN_SERVER="${PROTOCOL}admin.${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_ARCHIVE_DAYS=30 STRIPE_PUBLISHABLE_KEY="" @@ -25,7 +25,7 @@ ADMIN_SERVER="${PROTOCOL}admin.${DOMAIN_NAME}" ACCOUNT_SERVER="${PROTOCOL}account.${DOMAIN_NAME}" COOKIE_DOMAIN=".${DOMAIN_NAME}" SITE_NAME="OpenMHz" -ADMIN_EMAIL="luke@robotastic.com" +ADMIN_EMAIL="support@openmhz.com" S3_PROFILE='wasabi-account' S3_ENPOINT='s3.us-west-1.wasabisys.com' FREE_PLAN=0 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 0edabc9..d084127 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,6 +1,6 @@ # These are supported funding model platforms -github: robotastic +github: # Replace with a single Github username patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username diff --git a/account/server/config/express.js b/account/server/config/express.js index 5ddf39a..028b27c 100644 --- a/account/server/config/express.js +++ b/account/server/config/express.js @@ -5,10 +5,10 @@ const bodyParser = require("body-parser"); const MongoStore = require("connect-mongo"); 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 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 account_dev_server = "http://account.openmhz.test:3000" var admin_dev_server = "http://admin.openmhz.test:3000" diff --git a/account/server/controllers/users.js b/account/server/controllers/users.js index db9c243..42e9460 100644 --- a/account/server/controllers/users.js +++ b/account/server/controllers/users.js @@ -4,10 +4,10 @@ const Mailjet = require('node-mailjet'); 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 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({ diff --git a/account/server/index.js b/account/server/index.js index a127c49..e1dc8cc 100644 --- a/account/server/index.js +++ b/account/server/index.js @@ -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 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 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 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'; // ------------------------------------------- diff --git a/account/src/User/Login.js b/account/src/User/Login.js index 1f40944..3980db6 100644 --- a/account/src/User/Login.js +++ b/account/src/User/Login.js @@ -102,7 +102,7 @@ const Login = (props) => {
Let's get this started!
{process.env.REACT_APP_SITE_NAME} makes it easy to share recordings from{" "} - + Trunk Recorder .

diff --git a/account/src/User/Register.js b/account/src/User/Register.js index c6c408f..697d4bc 100644 --- a/account/src/User/Register.js +++ b/account/src/User/Register.js @@ -33,7 +33,7 @@ function Register(props) { Welcome!

Creating an account on OpenMHz is the first step in sharing radio system recordings using Trunk Recorder.

-

After creating an account, head over here to learn more about Trunk Recorder

+

After creating an account, head over here to learn more about Trunk Recorder

- Luke

diff --git a/account/src/User/Terms.js b/account/src/User/Terms.js index 38fb405..f3ef764 100644 --- a/account/src/User/Terms.js +++ b/account/src/User/Terms.js @@ -66,7 +66,7 @@ You agree not to sell, resell, or offer for any commercial purposes, any portion
5. API Access
-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
6. Legal Compliance
@@ -102,7 +102,7 @@ The Services are provided as a labor of love by a single individual. I have a li
14. Contact Me
-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! diff --git a/admin/server/config/express.js b/admin/server/config/express.js index 01d1632..a0762bd 100644 --- a/admin/server/config/express.js +++ b/admin/server/config/express.js @@ -5,10 +5,10 @@ const bodyParser = require("body-parser"); const MongoStore = require("connect-mongo"); 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 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" module.exports = function(app, passport) { diff --git a/admin/server/controllers/systems.js b/admin/server/controllers/systems.js index 44ef108..9dcfbd5 100644 --- a/admin/server/controllers/systems.js +++ b/admin/server/controllers/systems.js @@ -319,10 +319,14 @@ exports.uniqueShortName = async function (req, res, next) { res.status(500) res.json({ success: false, - message: "Short Name not provided in form" + message: "Shortname not provided in form" }); } + shortName = shortName.replace(/[^\w]/gi, ''); + shortName = shortName.toLowerCase(); + + try { var system = await System.findOne({ shortName: shortName.toLowerCase() @@ -333,7 +337,7 @@ exports.uniqueShortName = async function (req, res, next) { res.status(500) res.json({ success: false, - message: "Short Name already in use" + message: "Shortname already in use" }); return; } else { diff --git a/admin/src/Components/About.js b/admin/src/Components/About.js index b02bda7..22e2629 100644 --- a/admin/src/Components/About.js +++ b/admin/src/Components/About.js @@ -7,7 +7,7 @@ const About = () => { return (
Share the Air
-

{process.env.REACT_APP_SITE_NAME} makes it easy to share and archive recordings from Trunk Recorder. +

{process.env.REACT_APP_SITE_NAME} makes it easy to share and archive recordings from Trunk Recorder. 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 lead to greater appreciation for all the work they do that goes largely unseen.

diff --git a/backend/agents/otel-tracing.js b/backend/agents/otel-tracing.js deleted file mode 100644 index 1afb823..0000000 --- a/backend/agents/otel-tracing.js +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/backend/config/express.js b/backend/config/express.js index 00e29f2..d885f08 100644 --- a/backend/config/express.js +++ b/backend/config/express.js @@ -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 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" @@ -42,7 +42,9 @@ module.exports = function(app) { res.setHeader('Access-Control-Allow-Origin', origin); } else if (req.headers["user-agent"] == 'TrunkRecorder1.0') { 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', "*"); if (origin) { console.warn("forcing CORS for: " + origin + " referer: " + req.headers.referer + " url: " + req.originalUrl); diff --git a/backend/controllers/calls.js b/backend/controllers/calls.js index 6c0696d..2cdd10b 100644 --- a/backend/controllers/calls.js +++ b/backend/controllers/calls.js @@ -2,7 +2,6 @@ const { ObjectId } = require('mongodb'); const Call = require("../models/call"); const Group = require("../models/group"); -const opentelemetry = require('@opentelemetry/api'); var defaultNumResults = 50; var channels = {}; @@ -17,10 +16,12 @@ const build_call_list = (items) => { url: item.url, filename: item.path + item.name, time: item.time, - srcList: item.srcList, - star: item.star, + // Lean results skip schema defaults, so older documents can lack these. + srcList: item.srcList ?? [], + star: item.star ?? 0, + emergency: item.emergency, freq: item.freq, - patches: item.patches, + patches: item.patches ?? [], len: Math.round(item.len) }; calls.push(call); @@ -37,6 +38,7 @@ async function get_calls(query, numResults, middleDate, res) { path: true, name: true, time: true, + emergency: true, srcList: true, freq: true, patches: true, @@ -47,7 +49,7 @@ async function get_calls(query, numResults, middleDate, res) { const sort = { length: -1 }; 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); calls.push(...refined_items); @@ -57,7 +59,7 @@ async function get_calls(query, numResults, middleDate, res) { $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); calls.push(...refined_items); } @@ -210,11 +212,13 @@ function package_call(item) { time: item.time, timeString: timeString, dateString: dateString, + patches: item.patches, path: item.path, name: item.name, freq: item.freq, srcList: item.srcList, star: item.star, + emergency: item.emergency, len: Math.round(item.len) }; return call; diff --git a/backend/controllers/events.js b/backend/controllers/events.js index b8a3203..8d6897c 100644 --- a/backend/controllers/events.js +++ b/backend/controllers/events.js @@ -25,6 +25,8 @@ var s3_profile = process.env['S3_PROFILE'] != null ? process.env['S3_PROFILE'] : const client = new S3Client({ credentials: fromIni({ profile: s3_profile }), + requestChecksumCalculation: 'WHEN_REQUIRED', + responseChecksumValidation: 'WHEN_REQUIRED', endpoint: s3_endpoint, region: s3_region, maxAttempts: 2 diff --git a/backend/controllers/systems.js b/backend/controllers/systems.js index f54c75a..04aee07 100644 --- a/backend/controllers/systems.js +++ b/backend/controllers/systems.js @@ -4,7 +4,7 @@ var System = require("../models/system"); const Mailjet = require('node-mailjet'); 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 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); } +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) { /* going to use a cron job to update the system list every 2 minutes diff --git a/backend/controllers/uploads.js b/backend/controllers/uploads.js index 3f051f6..f99771a 100644 --- a/backend/controllers/uploads.js +++ b/backend/controllers/uploads.js @@ -1,18 +1,18 @@ const path = require('path'); const fs = require('fs'); -const opentelemetry = require('@opentelemetry/api'); const sysStats = require("../sys_stats"); const systemSchema = require("../models/systemSchema"); const talkgroupSchema = require("../models/talkgroupSchema"); const callSchema = require("../models/callSchema"); -const { trace, context } = opentelemetry; const mongoose = require("mongoose"); const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3"); const { fromIni } = require("@aws-sdk/credential-providers"); const { NodeHttpHandler } = require('@smithy/node-http-handler'); const https = require('https'); +// keepAlive matters: without it every upload does a full TCP + TLS handshake to S3. const agent = new https.Agent({ + keepAlive: true, maxSockets: 250, }); @@ -52,13 +52,34 @@ const client = new S3Client({ httpsAgent: agent, }), credentials: fromIni({ profile: s3_profile }), + requestChecksumCalculation: 'WHEN_REQUIRED', + responseChecksumValidation: 'WHEN_REQUIRED', endpoint: s3_endpoint, region: s3_region, 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) { - const tracer = trace.getTracer("upload-service"); let start_time = Date.now(); let validateSystemTime = 0; let readFileTime = 0; @@ -66,217 +87,163 @@ exports.upload = async function (req, res, next) { let saveCallTime = 0; let cleanupTime = 0; let statsTime = 0; - return context.with(context.active(), async () => { - const parentSpan = trace.getActiveSpan(context.active()); - await tracer.startActiveSpan('upload_handler', { parent: parentSpan }, async (span) => { - try { - span.setAttribute('call.shortName', req.params.shortName.toLowerCase()); - span.setAttribute('call.talkgroup_num', req.body.talkgroup_num); - span.setAttribute('call.start_time', req.body.start_time); + try { + if (!req.file || !['.m4a', '.mp3'].includes(path.extname(req.file.originalname))) { + console.warn(`[${req.params.shortName}] Error file name is wrong or file does not exist`); + res.status(500).send("Error, invalid filename:\n"); + return; + } - if (!req.file || !['.m4a', '.mp3'].includes(path.extname(req.file.originalname))) { - console.warn(`[${req.params.shortName}] Error file name is wrong or file does not exist`); - span.setStatus({ - code: opentelemetry.SpanStatusCode.ERROR, - message: "Invalid file or filename", - }); - res.status(500).send("Error, invalid filename:\n"); - return; - } + const shortName = req.params.shortName.toLowerCase(); + const apiKey = req.body.api_key; + 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); - const shortName = req.params.shortName.toLowerCase(); - const apiKey = req.body.api_key; - 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 spikeCount = parseInt(req.body.spike_count) || 0; - let errorCount = parseInt(req.body.error_count) || 0; - let spikeCount = parseInt(req.body.spike_count) || 0; + let srcList = []; + 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 = []; - 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 = []; - patches = []; + let req_patches; + req_patches = req.body.patch_list; - let req_patches; - req_patches = req.body.patch_list; - - if(typeof req_patches != "undefined"){ - var split_patches = req_patches.replace("[","").replace("]","").split(","); - - for (var patch in split_patches){ - patches.push(split_patches[patch]); - } - } + if(typeof req_patches != "undefined"){ + var split_patches = req_patches.replace("[","").replace("]","").split(","); - 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(); + for (var patch in split_patches){ + patches.push(split_patches[patch]); } + } + 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, }); - }); -}; \ No newline at end of file + + 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}`); + } + } +}; diff --git a/backend/index.js b/backend/index.js index 2f328a3..bd08608 100644 --- a/backend/index.js +++ b/backend/index.js @@ -1,4 +1,3 @@ -const tracing = require("./agents/otel-tracing"); var express = require("express"); var configureExpress = require("./config/express"); @@ -25,6 +24,12 @@ var app = express(); 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, { 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_user = process.env['MONGO_USER']; const mongo_password = process.env['MONGO_PASSWORD']; +const firehose_key = process.env['FIREHOSE_KEY'] !== 'undefined' ? process.env['FIREHOSE_KEY'] : null; let mongoUrl; @@ -59,20 +65,20 @@ if ((typeof mongo_user !== 'undefined') && (typeof mongo_password !== 'undefined const connect = async () => { - // Demonstrate the readyState and on event emitters - console.log(mongoose.connection.readyState); //logs 0 - mongoose.connection.on('connecting', () => { - console.log('Mongoose is connecting') - }); - mongoose.connection.on('connected', () => { - console.log('Mongoose is connected'); - }); - mongoose.connection.on('disconnecting', () => { - console.log('Mongoose is disconnecting'); - }); - - await mongoose.connect(mongoUrl, { useNewUrlParser: true, useUnifiedTopology: true, maxPoolSize: 50 }); - console.log("All Done"); + // Demonstrate the readyState and on event emitters + console.log(mongoose.connection.readyState); //logs 0 + mongoose.connection.on('connecting', () => { + console.log('Mongoose is connecting') + }); + mongoose.connection.on('connected', () => { + console.log('Mongoose is connected'); + }); + mongoose.connection.on('disconnecting', () => { + console.log('Mongoose is disconnecting'); + }); + + await mongoose.connect(mongoUrl, { useNewUrlParser: true, useUnifiedTopology: true, maxPoolSize: 50 }); + console.log("All Done"); } connect(); @@ -106,7 +112,6 @@ function addSystemClients(req, res, next) { } function addTotalClients(req, res, next) { - req.totalClients = Object.keys(clients).length; next(); } @@ -123,7 +128,7 @@ app.get('/:shortName/calls/newer', calls.get_newer_calls); app.get('/:shortName/calls/older', calls.get_older_calls); app.get('/:shortName/calls/date', calls.get_date_calls); app.get('/:shortName/calls/:time/older', calls.get_iphone_calls); -app.get('/:shortName/calls', calls.get_calls); +app.get('/:shortName/calls', calls.get_calls); /*------ UPLOADS ---------- upload.single('call'), uploads.upload,*/ @@ -152,6 +157,9 @@ app.get('/events/:id', events.getEvent); app.get('/:shortName/stats', stats.get_stats); app.get('/stats', addTotalClients, sys_stats.siteStats) +/*------ SYSTEM STATUS ----------*/ +app.get('/status/:shortName', systems.get_system_status); + function get_clients(req, res) { if (req.params.shortName) { @@ -190,62 +198,53 @@ function get_clients(req, res) { app.get('/clients', get_clients); app.get('/:shortName/clients', get_clients); app.use(function (err, req, res, next) { - console.error("Caught an error"); - console.error(err.stack); - /* - res.status(err.status || 500); - res.contentType('json'); - res.send(JSON.stringify({ - message: err.message, - error: err - }));*/ + + console.log("Error: " + err); }); function notify_clients(call) { call.type = "calls"; + const message = JSON.stringify(call); var sent = 0; for (var key in clients) { if (clients.hasOwnProperty(key)) { var client = clients[key]; if (client.active) { - if (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.filterStarred || call.star) { - if (client.filterCode == "") { - sent++; - client.socket.emit("new message", JSON.stringify(call)); - } else if (client.filterType == "unit") { - var codeArray = client.filterCode.split(','); - var success = false; - for (var j = 0; j < codeArray.length; ++j) { - for (var k = 0; k < call.srcList.length; k++) { - if (codeArray[j] == call.srcList[k]) { - sent++; - client.socket.emit("new message", JSON.stringify(call)); - success = true; - break; - } - } - if (success) { + if ((client.firehose == true) || (client.shortName == call.shortName.toLowerCase())) { + // if client + if ((client.firehose == true) || (client.filterType == "all")) { + sent++; + client.socket.emit("new message", message); + } else if (client.filterType == "unit") { + var codeArray = client.filterCode.split(','); + var success = false; + for (var j = 0; j < codeArray.length; ++j) { + for (var k = 0; k < call.srcList.length; k++) { + if (codeArray[j] == call.srcList[k]) { + sent++; + client.socket.emit("new message", message); + success = true; break; } } - - - } else { - var codeArray = client.talkgroupNums; - for (var j = 0; j < codeArray.length; ++j) { - if (codeArray[j] == call.talkgroupNum) { - client.socket.emit("new message", JSON.stringify(call)); - sent++ - break; - } + if (success) { + break; + } + } + } else { + var codeArray = client.talkgroupNums; + for (var j = 0; j < codeArray.length; ++j) { + if (codeArray[j] == call.talkgroupNum) { + 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) { + 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].timestamp = new Date(); 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].shortName = data.shortName.toLowerCase(); 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].talkgroupNums = []; 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") { - 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]; return; } - if ((data.filterType == "group") && (typeof data.filterCode == "string") && (data.filterCode.indexOf(',') == -1)) { - if (!ObjectId.isValid(data.filterCode)) { - console.error("Error - Socket - Invalid Group ID: " + data.filterCode); + if (typeof clients[client.id].filterType != "string") { + console.error("Error - Socket - Invalid filterType: " + data.filterType + " ShortName: " + data.shortName.toLowerCase() + " ClientID: " + client.id + " IP: " + ipAddress); + 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]; 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) }); if (typeof clients[client.id] === "undefined") { - console.error("How can it be undefined here!!"); - console.error(client.id); + console.error("Error - Socket - Client not found ShortName: " + data.shortName + " ClientID: " + client.id + " IP: " + ipAddress); return; } if (group) { clients[client.id].talkgroupNums = group.talkgroups; } 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]; return; } - + // Handle filterType == "talkgroup" } else if ((data.filterType == "talkgroup") && Array.isArray(data.filterCode)) { clients[client.id].talkgroupNums = data.filterCode; + } else if ((data.filterType == "all") && (data.filterCode == "")) { + // Handle filterType == "all" + clients[client.id].talkgroupNums = []; } else { - // This is for ALL filters - //console.error("Invalid Socket Filter - Type: " + data.filterType + " Code: " + data.filterCode); + 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); + 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 { - 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) { if (clients[client.id]) { clients[client.id].active = false; @@ -324,15 +364,15 @@ io.sockets.on('connection', function (client) { stats.init_stats(); // 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 -schedule.scheduleJob('0 3 * * *', function() { +schedule.scheduleJob('0 3 * * *', function () { db.cleanOldCalls(); }); -schedule.scheduleJob('15 3 * * *', function() { +schedule.scheduleJob('15 3 * * *', function () { db.cleanOldEvents(); }); -schedule.scheduleJob('30 3 * * *', function() { +schedule.scheduleJob('30 3 * * *', function () { db.cleanOldPodcasts(); }); diff --git a/backend/models/callSchema.js b/backend/models/callSchema.js index c0e6559..4c845bf 100644 --- a/backend/models/callSchema.js +++ b/backend/models/callSchema.js @@ -1,6 +1,6 @@ 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({ talkgroupNum: Number, diff --git a/docker-compose.yml b/docker-compose.yml index c96b4bd..57ad39d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,7 +25,6 @@ services: - frontend - account - 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: image: certbot/certbot:latest @@ -73,6 +72,7 @@ services: - MONGO_PASSWORD - MAILJET_KEY - MAILJET_SECRET + - FIREHOSE_KEY - REACT_APP_ADMIN_EMAIL - REACT_APP_SITE_NAME - NODE_ENV diff --git a/frontend/public/app-terms.html b/frontend/public/app-terms.html index ed4eddb..4750c00 100644 --- a/frontend/public/app-terms.html +++ b/frontend/public/app-terms.html @@ -107,7 +107,7 @@

Contact Us

If you have any questions or suggestions about my Terms and Conditions, do not hesitate to contact me - at luke@robotastic.com. + at support@openmhz.com.

This Terms and Conditions page was generated by App Privacy Policy Generator

diff --git a/frontend/public/privacy.html b/frontend/public/privacy.html index bd93e91..1d559df 100644 --- a/frontend/public/privacy.html +++ b/frontend/public/privacy.html @@ -87,7 +87,7 @@ this page.

This policy is effective as of 2023-09-16

Contact Us

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.

This privacy policy page was created at privacypolicytemplate.net and modified/generated by App Privacy Policy Generator

diff --git a/frontend/server/config/express.js b/frontend/server/config/express.js index 86f9477..21ec263 100644 --- a/frontend/server/config/express.js +++ b/frontend/server/config/express.js @@ -5,10 +5,10 @@ var favicon = require('serve-favicon') 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 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" module.exports = function(app) { diff --git a/frontend/server/index.js b/frontend/server/index.js index 9f8a60c..47f53e5 100644 --- a/frontend/server/index.js +++ b/frontend/server/index.js @@ -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 = ` - - - - <![CDATA[ OpenMHz ]]> - - - 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. ]]> - - https://openmhz.com - Podcast for Node - ${now} - - - - - ${now} - - - - - - - 60 - OpenMHz - I am a sub title - 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. - - Luke Berndt - luke@robotastic.com - - false - - - - - ` - - - - const feedEnd = ` - - -` - 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 + ` - - -<![CDATA[ ${podcast.title} ]]> - - - - -${podcast.downloadUrl} - - - -${podcast.eventUrl} -${podcast.startTime} - -OpenMHz -${description} -false - -` - } - - - xml = xml + feedEnd; - res.set('Content-Type', 'text/xml'); - res.send(xml); - }); - - } catch (err) { - console.error(err); - return; - } -}*/ const dateRange = (start, end) => { 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' }) }; @@ -227,14 +97,14 @@ async function getPodcast(req, res, next) { siteUrl: 'https://openmhz.com', imageUrl: 'https://openmhz.com/podcast/cover.png', author: 'OpenMHz', - copyright: '© 2022 Robotastic', + copyright: '© 2026 Robotastic', language: 'en', pubDate: 'May 20, 2012 04:00:00 GMT', ttl: 60, itunesAuthor: 'OpenMHz', 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.', - itunesOwner: { name: 'Luke Berndt', email: 'luke@robotastic.com' }, + itunesOwner: { name: 'Luke Berndt', email: 'support@openmhz.com' }, itunesExplicit: false, itunesCategory: [{ text: 'Government' diff --git a/frontend/src/About/AboutComponent.js b/frontend/src/About/AboutComponent.js index dc1c0d5..04e54a6 100644 --- a/frontend/src/About/AboutComponent.js +++ b/frontend/src/About/AboutComponent.js @@ -17,7 +17,7 @@ const AboutComponent = () => {
About
-

I wrote Trunk Recorder because I was curious +

I wrote Trunk Recorder because I was curious 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.

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 = () => {

100% Open Source

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:

Trunk Server

-
Support
-

If OpenMHz brings you joy, think about becoming a supporter! It will cover hosting costs and help keep me focused on development.

- -

+
FAQ
@@ -45,7 +39,7 @@ const AboutComponent = () => { Where do the different feeds come from - It is a team effort! There are contributors from around country and even internationally, that are running Trunk Recorder 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 Trunk Recorder to capture local radio systems and sending the transmissions to OpenMHz. @@ -98,7 +92,7 @@ const AboutComponent = () => { I have other questions

- Check out the Terms of Service, there might be an answer there or send me an email at luke@robotastic.com + Check out the Terms of Service, there might be an answer there or send me an email at support@openmhz.com

diff --git a/frontend/src/About/Terms.js b/frontend/src/About/Terms.js index 8e82628..62fad25 100644 --- a/frontend/src/About/Terms.js +++ b/frontend/src/About/Terms.js @@ -55,7 +55,7 @@ You agree not to sell, resell, or offer for any commercial purposes, any portion
5. API Access
-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
6. Legal Compliance
@@ -91,7 +91,7 @@ The Services are provided as a labor of love by a single individual. I have a li
14. Contact Me
-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! diff --git a/frontend/src/Call/CallInfo.js b/frontend/src/Call/CallInfo.js index 7ba5b48..f8fb960 100644 --- a/frontend/src/Call/CallInfo.js +++ b/frontend/src/Call/CallInfo.js @@ -33,7 +33,7 @@ function CallInfo(props) { callFreq = Math.round(freq * 1000) / 1000; } - srcList = currentCall.srcList.map((source, index) => {source.src}[{source.pos}]); + srcList = currentCall.srcList.map((source, index) => {source.src}[{source.pos}] {source.tag ? "(" + source.tag + ")" : ""}); callLength = currentCall.len; talkgroupNum = currentCall.talkgroupNum; callDownload = currentCall.url; diff --git a/frontend/src/Call/CallPlayer.js b/frontend/src/Call/CallPlayer.js index 9ec0a84..fe4c121 100644 --- a/frontend/src/Call/CallPlayer.js +++ b/frontend/src/Call/CallPlayer.js @@ -177,6 +177,19 @@ function CallPlayer(props) { } }, [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 = () => { setStatusVisible(false); } diff --git a/frontend/src/Call/components/CallInfo.js b/frontend/src/Call/components/CallInfo.js index ec677f5..fcb5348 100644 --- a/frontend/src/Call/components/CallInfo.js +++ b/frontend/src/Call/components/CallInfo.js @@ -44,7 +44,7 @@ function CallInfo(props) { } - srcList = currentCall.srcList.map((source, index) => {source.src}[{source.pos}]); + srcList = currentCall.srcList.map((source, index) => {source.src}[{source.pos}] {source.tag ? "(" + source.tag + ")" : ""}); callLength = currentCall.len; talkgroupNum = currentCall.talkgroupNum; diff --git a/frontend/src/Call/components/CallInfoPane.js b/frontend/src/Call/components/CallInfoPane.js index fdb581b..450ccc8 100644 --- a/frontend/src/Call/components/CallInfoPane.js +++ b/frontend/src/Call/components/CallInfoPane.js @@ -26,6 +26,7 @@ function CallInfoPane(props) { let patchString = ""; let header = "Call Info" let title = "" + let callEmergnecy = (<> ); const currentCall = props.call ? props.call : false; const { callLink, callDownload, callTweet } = useCallLink(props.call) const { data: allSystems, isSuccess } = useGetSystemsQuery(); @@ -50,12 +51,16 @@ function CallInfoPane(props) { const freq = currentCall.freq / 1000000; callFreq = Math.round(freq * 1000) / 1000; } - srcList = currentCall.srcList.map((source, index) => {source.src}[{source.pos}]); + srcList = currentCall.srcList.map((source, index) => {source.src}[{source.pos}] {source.tag ? "(" + source.tag + ")" : ""}); callLength = currentCall.len; talkgroupNum = currentCall.talkgroupNum; patches = currentCall.patches; + + if (currentCall.emergency) { + callEmergnecy = (
Emergency
) + } - if(patches.length > 1) { + if(patches && (patches.length > 1)) { patchString = patches.join(", "); } else{ @@ -100,6 +105,7 @@ function CallInfoPane(props) { return ( <>
{header}
+ {callEmergnecy} {srcList} diff --git a/frontend/src/Call/components/CallItem.js b/frontend/src/Call/components/CallItem.js index 7c182d7..74c87c6 100644 --- a/frontend/src/Call/components/CallItem.js +++ b/frontend/src/Call/components/CallItem.js @@ -105,6 +105,14 @@ const CallItem = (props) => { key: "blue", inverted: "true" } + } else if (call.emergency) { + rowSelected = { + positive: true, + error: true, + color: "red", + key: "red", + inverted: "true" + } } let talkgroup; if ((typeof talkgroups== 'undefined') || (typeof talkgroups.talkgroups[call.talkgroupNum] == 'undefined')) { diff --git a/frontend/src/Call/components/MediaPlayer.css b/frontend/src/Call/components/MediaPlayer.css index e9c4772..b6d3235 100644 --- a/frontend/src/Call/components/MediaPlayer.css +++ b/frontend/src/Call/components/MediaPlayer.css @@ -21,11 +21,16 @@ .label-item { padding: 9px 2px 5px 2px; - width: 130px; + width: 150px; color: white; font-weight: bold; text-align: center; } + .tag-label { + max-width: 75px; + overflow: clip; + text-wrap-mode: nowrap; + } .mediaplayer { flex-grow: 1; color: white; diff --git a/frontend/src/Call/components/MediaPlayer.js b/frontend/src/Call/components/MediaPlayer.js index 5250e9b..6d4b358 100644 --- a/frontend/src/Call/components/MediaPlayer.js +++ b/frontend/src/Call/components/MediaPlayer.js @@ -136,10 +136,14 @@ const MediaPlayer = (props) => { let playEnabled = { "disabled": true } let sourceId = "-"; + let sourceTag = ""; if (call) { if (call.srcList.length > sourceIndex) { 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 = {}; } @@ -200,8 +204,8 @@ const MediaPlayer = (props) => { {playTime} Sec -