Merge pull request #126 from openmhz/refactor

Refactor
This commit is contained in:
Luke Berndt
2023-01-13 16:42:44 -05:00
committed by GitHub
37 changed files with 6855 additions and 11269 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -99,9 +99,9 @@ On MacOS, make the changes in: */etc/hosts*
# when the system is booting. Do not change this entry. # when the system is booting. Do not change this entry.
## ##
127.0.0.1 localhost 127.0.0.1 localhost
127.0.0.1 openmhz.test api.openmhz.test admin.openmhz.test media.openmhz.test 127.0.0.1 openmhz.test account.openmhz.test api.openmhz.test admin.openmhz.test media.openmhz.test
```` ````
Then load these values into local DNS: `dscacheutil –flushcache` Then load these values into local DNS: `sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder`
- In root dir, run: `./docker-test.sh build` - In root dir, run: `./docker-test.sh build`
@@ -144,6 +144,27 @@ From the Host OS run:
```bash ```bash
docker exec -i -t $(docker ps -a | grep mongo | awk '{print $1}') /bin/bash docker exec -i -t $(docker ps -a | grep mongo | awk '{print $1}') /bin/bash
``` ```
Then launch the `mongo` CLI tool and find any users you have created
```bash
mongo
use scanner
db.users.find()
```
Now swap out the ObjectId for the user, and then run this command. It will make it so you don't need to confirm the email.
```bash
db.users.updateOne(
{ "_id" : ObjectId("63a620d0a63b087b005f6726") },
{
$set: { "confirmEmail" : true },
$currentDate: { lastModified: true }
}
)
```
There are a few scripts included with the container: There are a few scripts included with the container:
- **clean.js** This script removes all Calls that are over 30 days old - **clean.js** This script removes all Calls that are over 30 days old

View File

@@ -47,6 +47,7 @@
"mongoose": "5.11.10", "mongoose": "5.11.10",
"node-mailjet": "3.3.1", "node-mailjet": "3.3.1",
"passport": "0.4.1", "passport": "0.4.1",
"passport-local": "1.0.0" "passport-local": "1.0.0",
"passport-twitter": "1.0.4"
} }
} }

View File

@@ -0,0 +1,18 @@
var Strategy = require('passport-twitter').Strategy;
const User = require("../../models/user");
const twitter = new Strategy({
consumerKey: process.env['TWITTER_CONSUMER_KEY'],
consumerSecret: process.env['TWITTER_CONSUMER_SECRET'],
callbackURL: '/oauth/callback'
},
function(token, tokenSecret, profile, cb) {
// In this example, the user's Twitter profile is supplied as the user
// record. In a production-quality application, the Twitter profile should
// be associated with a user record in the application's database, which
// allows for account linking and authentication with other identity
// providers.
return cb(null, profile);
})
module.exports = local;

File diff suppressed because one or more lines are too long

View File

@@ -235,14 +235,7 @@ exports.upsertGroup = function(req, res, next) {
var message = ""; var message = "";
var groupName = req.body.groupName.replace(/[^\w\s]/g, ""); var groupName = req.body.groupName.replace(/[^\w\s]/g, "");
var talkgroups = JSON.parse(req.body.talkgroups); var talkgroups = JSON.parse(req.body.talkgroups);
console.log(
"Talkgroups: " +
talkgroups +
" for group: " +
groupName +
" array: " +
Array.isArray(talkgroups)
);
if (!talkgroups.length || !Array.isArray(talkgroups)) { if (!talkgroups.length || !Array.isArray(talkgroups)) {
message = "Talkgroups need to be selected, group can not be empty "; message = "Talkgroups need to be selected, group can not be empty ";
} }

View File

@@ -50,7 +50,8 @@ exports.listSystems = function (req, res, next) {
country, country,
userId, userId,
key, key,
showScreenName showScreenName,
ignoreUnknownTalkgroup
}) => ({ }) => ({
name, name,
shortName, shortName,
@@ -62,7 +63,8 @@ exports.listSystems = function (req, res, next) {
country, country,
userId, userId,
key, key,
showScreenName showScreenName,
ignoreUnknownTalkgroup
}))(obj); }))(obj);
if (obj.showScreenName) { if (obj.showScreenName) {
rObj.screenName = req.user.screenName; rObj.screenName = req.user.screenName;
@@ -203,11 +205,7 @@ exports.ownSystem = async function (req, res, next) {
console.error("System: " + system.userId + " User: " + req.user._id); console.error("System: " + system.userId + " User: " + req.user._id);
return; return;
} }
res.locals.test = "Yoho!"
res.locals.system = system; res.locals.system = system;
console.log(system);
console.log(res.locals);
next(); next();
} catch (err) { } catch (err) {
@@ -271,7 +269,6 @@ exports.uniqueShortName = async function (req, res, next) {
exports.updateSystem = async function (req, res, next) { exports.updateSystem = async function (req, res, next) {
console.log(res.locals)
res.locals.system.name = res.locals.name; res.locals.system.name = res.locals.name;
res.locals.system.city = res.locals.city; res.locals.system.city = res.locals.city;
res.locals.system.state = res.locals.state; res.locals.system.state = res.locals.state;
@@ -280,6 +277,7 @@ exports.updateSystem = async function (req, res, next) {
res.locals.system.description = res.locals.description; res.locals.system.description = res.locals.description;
res.locals.system.systemType = res.locals.systemType; res.locals.system.systemType = res.locals.systemType;
res.locals.system.showScreenName = res.locals.showScreenName; res.locals.system.showScreenName = res.locals.showScreenName;
res.locals.system.ignoreUnknownTalkgroup = res.locals.ignoreUnknownTalkgroup;
res.locals.system.save(function (err) { res.locals.system.save(function (err) {
if (err) { if (err) {
@@ -300,7 +298,8 @@ exports.updateSystem = async function (req, res, next) {
country, country,
userId, userId,
key, key,
showScreenName showScreenName,
ignoreUnknownTalkgroup
}) => ({ }) => ({
name, name,
shortName, shortName,
@@ -312,7 +311,8 @@ exports.updateSystem = async function (req, res, next) {
country, country,
userId, userId,
key, key,
showScreenName showScreenName,
ignoreUnknownTalkgroup
}))(res.locals.system); }))(res.locals.system);
returnSys.id = res.locals.system._id; returnSys.id = res.locals.system._id;
res.json({ res.json({
@@ -329,7 +329,7 @@ exports.updateSystem = async function (req, res, next) {
exports.validateSystem = async function (req, res, next) { exports.validateSystem = async function (req, res, next) {
try { try {
res.locals.showScreenName = req.body.showScreenName; res.locals.showScreenName = req.body.showScreenName;
res.locals.ignoreUnknownTalkgroup = req.body.ignoreUnknownTalkgroup ;
if (!req.body.name || (req.body.name.length < 2)) { if (!req.body.name || (req.body.name.length < 2)) {
console.error("ERROR: Validate System - req.body.name"); console.error("ERROR: Validate System - req.body.name");
res.json({ res.json({
@@ -456,7 +456,8 @@ exports.createSystem = async function (req, res, next) {
state, state,
county, county,
country, country,
showScreenName showScreenName,
ignoreUnknownTalkgroup
}) => ({ }) => ({
name, name,
shortName, shortName,
@@ -466,7 +467,8 @@ exports.createSystem = async function (req, res, next) {
state, state,
county, county,
country, country,
showScreenName showScreenName,
ignoreUnknownTalkgroup
}))(res.locals); }))(res.locals);
system.key = key; system.key = key;
system.userId = mongoose.Types.ObjectId(req.user._id); system.userId = mongoose.Types.ObjectId(req.user._id);
@@ -490,6 +492,7 @@ exports.createSystem = async function (req, res, next) {
country, country,
userId, userId,
showScreenName, showScreenName,
ignoreUnknownTalkgroup ,
key key
}) => ({ }) => ({
name, name,
@@ -502,6 +505,7 @@ exports.createSystem = async function (req, res, next) {
country, country,
userId, userId,
showScreenName, showScreenName,
ignoreUnknownTalkgroup ,
key key
}))(newSys); }))(newSys);
returnSys.id = newSys._id; returnSys.id = newSys._id;

View File

@@ -11,6 +11,7 @@ var systemSchema = mongoose.Schema({
state: String, state: String,
description: String, description: String,
showScreenName: Boolean, showScreenName: Boolean,
ignoreUnknownTalkgroup : Boolean,
userId: mongoose.Schema.Types.ObjectId, userId: mongoose.Schema.Types.ObjectId,
key: String key: String
}); });

View File

@@ -13,7 +13,8 @@ class SystemForm extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.handleInputChange = this.handleInputChange.bind(this); this.handleInputChange = this.handleInputChange.bind(this);
this.handleCheckboxChange = this.handleCheckboxChange.bind(this); this.handleScreenNameCheckboxChange = this.handleScreenNameCheckboxChange.bind(this);
this.handleTalkgroupCheckboxChange = this.handleTalkgroupCheckboxChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this);
this.checkInputs = this.checkInputs.bind(this); this.checkInputs = this.checkInputs.bind(this);
this.render = this.render.bind(this); this.render = this.render.bind(this);
@@ -30,6 +31,7 @@ class SystemForm extends Component {
county: "", county: "",
country: "", country: "",
showScreenName: false, showScreenName: false,
ignoreUnknownTalkgroup: false,
nameError: false, nameError: false,
shortNameError: false, shortNameError: false,
descriptionError: false, descriptionError: false,
@@ -51,6 +53,7 @@ class SystemForm extends Component {
this.state.county = this.props.system.county; this.state.county = this.props.system.county;
this.state.country = this.props.system.country; this.state.country = this.props.system.country;
this.state.showScreenName = this.props.system.showScreenName; this.state.showScreenName = this.props.system.showScreenName;
this.state.ignoreUnknownTalkgroup = this.props.system.ignoreUnknownTalkgroup;
} }
} }
// If we directly to an updating url, the system fetch will come back after the constructer, so we need to catch this and update the values // If we directly to an updating url, the system fetch will come back after the constructer, so we need to catch this and update the values
@@ -66,6 +69,7 @@ class SystemForm extends Component {
county: this.props.system.county, county: this.props.system.county,
country: this.props.system.country, country: this.props.system.country,
showScreenName: this.props.system.showScreenName, showScreenName: this.props.system.showScreenName,
ignoreUnknownTalkgroup: this.props.system.ignoreUnknownTalkgroup,
}); });
} }
} }
@@ -148,10 +152,13 @@ class SystemForm extends Component {
} }
handleInputChange = (e, { name, value }) => this.setState({ [name]: value }); handleInputChange = (e, { name, value }) => this.setState({ [name]: value });
handleCheckboxChange() { handleScreenNameCheckboxChange() {
this.setState({ showScreenName: !this.state.showScreenName }); this.setState({ showScreenName: !this.state.showScreenName });
} }
handleTalkgroupCheckboxChange() {
this.setState({ ignoreUnknownTalkgroup: !this.state.ignoreUnknownTalkgroup });
}
handleSubmit(event) { handleSubmit(event) {
event.preventDefault(); event.preventDefault();
let inputError = this.checkInputs(); let inputError = this.checkInputs();
@@ -166,7 +173,8 @@ class SystemForm extends Component {
state, state,
county, county,
country, country,
showScreenName showScreenName,
ignoreUnknownTalkgroup
}) => ({ }) => ({
name, name,
shortName, shortName,
@@ -176,7 +184,8 @@ class SystemForm extends Component {
state, state,
county, county,
country, country,
showScreenName showScreenName,
ignoreUnknownTalkgroup
}))(this.state); }))(this.state);
this.props.onSubmit(system); this.props.onSubmit(system);
} }
@@ -294,13 +303,16 @@ class SystemForm extends Component {
</Form.Group> </Form.Group>
<Divider horizontal /> <Divider horizontal />
<Form.Field> <Form.Field>
<p>Your Screen Name: {this.props.screenName}</p>
<Checkbox <Checkbox
onChange={this.handleCheckboxChange} onChange={this.handleScreenNameCheckboxChange}
checked={this.state.showScreenName} checked={this.state.showScreenName}
label="Show in System Listing" /> label="Display Screen Name in System Listing" />
</Form.Field>
<Form.Field>
<Checkbox
onChange={this.handleTalkgroupCheckboxChange}
checked={this.state.ignoreUnknownTalkgroup}
label="Ignore Unknown Talkgroups" />
</Form.Field> </Form.Field>
<Divider horizontal /> <Divider horizontal />
<Form.TextArea <Form.TextArea
@@ -386,19 +398,19 @@ class SystemForm extends Component {
/> />
</Form.Field> </Form.Field>
) : ( ) : (
<Form.Field> <Form.Field>
<Form.Dropdown <Form.Dropdown
name="state" name="state"
onChange={this.handleInputChange} onChange={this.handleInputChange}
error={this.state.stateError} error={this.state.stateError}
value={this.state.state} value={this.state.state}
options={stateOptions} options={stateOptions}
scrolling scrolling
label="State" label="State"
placeholder="Select State..." placeholder="Select State..."
/> />
</Form.Field> </Form.Field>
)} )}
</Form.Group> </Form.Group>

View File

@@ -71,7 +71,8 @@ const system = (
key: action.data.key, key: action.data.key,
userId: action.data.userId, userId: action.data.userId,
id: action.data.id, id: action.data.id,
showScreenName: action.data.showScreenName showScreenName: action.data.showScreenName,
ignoreUnknownTalkgroup : action.data.ignoreUnknownTalkgroup
}); });
return Object.assign({}, state, { isWaiting: false, return Object.assign({}, state, { isWaiting: false,
items: newItems}); items: newItems});

View File

@@ -64,9 +64,11 @@ process.nextTick(function() {
var shortName = req.params.shortName.toLowerCase(); var shortName = req.params.shortName.toLowerCase();
var apiKey = req.body.api_key; var apiKey = req.body.api_key;
System.findOne({ System.findOne({
'shortName': shortName 'shortName': shortName
}, async function(err, item) { },["key", "ignoreUnknownTalkgroup"], async function(err, item) {
if (err) { if (err) {
console.warn("[" + req.params.shortName + "] Error /:shortName/upload - Error: " + err); console.warn("[" + req.params.shortName + "] Error /:shortName/upload - Error: " + err);
@@ -98,6 +100,29 @@ process.nextTick(function() {
var emergency = parseInt(req.body.emergency); var emergency = parseInt(req.body.emergency);
if (item.ignoreUnknownTalkgroup == true) {
//console.log("ignore unknown talkgroup ");
//await Talkgroup.findOne({ answer: 42 }).select({ _id: 1 }).lean().then(doc => !!doc)
talkgroupExists = await Talkgroup.exists({
'shortName': shortName,
'num': talkgroupNum
});
//console.log("In talkgroup: " + talkgroupExists);
//talkgroupExists = await Talkgroup.findOne({ 'shortName': shortName, 'num': talkgroupNum }).select({ _id: 1 }).lean().then(doc => !!doc).exec();
if (!talkgroupExists) {
try {
fs.unlinkSync(req.file.path)
//file removed
} catch(err) {
console.log("[" + call.shortName + "] error deleting: " + req.file.path);
}
res.status(500);
res.send("Talkgroup does not exist, skipping.\n");
return;
}
}
// Add in an API Key check // Add in an API Key check
try { try {
@@ -107,7 +132,6 @@ process.nextTick(function() {
// Mongoose does not allow 'errors' to be used in the Schema for call. need to rename to error. // Mongoose does not allow 'errors' to be used in the Schema for call. need to rename to error.
var badFreqList = JSON.parse(req.body.freq_list); var badFreqList = JSON.parse(req.body.freq_list);
var freqList = badFreqList.map(obj =>{ var freqList = badFreqList.map(obj =>{
obj.error = obj.errors; obj.error = obj.errors;
delete obj.errors; delete obj.errors;
return obj; return obj;

View File

@@ -32,6 +32,7 @@ const server = require('http').createServer(app);
const io = require('socket.io')(server, {}); const io = require('socket.io')(server, {});
var Twitter = require('twitter-lite');
io.origins('*:*'); io.origins('*:*');
@@ -43,50 +44,71 @@ io.origins((origin, callback) => {
callback(null, true); callback(null, true);
});*/ });*/
io.on('connection', function(socket){ io.on('connection', function (socket) {
//console.log('a user connected'); //console.log('a user connected');
socket.on('disconnect', function(){ socket.on('disconnect', function () {
//console.log('user disconnected'); //console.log('user disconnected');
}); });
}); });
// This section of code enable a Twitter client to tweet out messages when a new call comes in
/*
const client = new Twitter({
subdomain: "api", // "api" is the default (change for other subdomains)
version: "1.1", // version "1.1" is the default (change for other subdomains)
consumer_key: process.env.TWITTER_CONSUMER_KEY, // from Twitter.
consumer_secret: process.env.TWITTER_CONSUMER_SECRET, // from Twitter.
access_token_key: process.env.TWITTER_ACCESS_TOKEN_KEY, // from your User (oauth_token)
access_token_secret: process.env.TWITTER_ACCESS_TOKEN_SECRET // from your User (oauth_token_secret)
});
client
.get("account/verify_credentials")
.then(results => {
console.log("Connected to Twitter");
})
.catch(console.error);
*/
function getRole(req) { function getRole(req) {
if (req.params.shortName && req.user) { if (req.params.shortName && req.user) {
var short_name = req.params.shortName.toLowerCase(); var short_name = req.params.shortName.toLowerCase();
var userId = req.user._id; var userId = req.user._id;
Permission.findOne({'userId': userId, 'shortName': short_name}, function(err, permission) { Permission.findOne({ 'userId': userId, 'shortName': short_name }, function (err, permission) {
if (err) { if (err) {
console.error('Error - getRole userId: ' + userId + ' shortName: ' + short_name + ' error: ' + err); console.error('Error - getRole userId: ' + userId + ' shortName: ' + short_name + ' error: ' + err);
return -1; return -1;
} }
console.log('Found - getRole userId: ' + userId + ' shortName: ' + short_name + ' role: ' + permission.role); console.log('Found - getRole userId: ' + userId + ' shortName: ' + short_name + ' role: ' + permission.role);
return permission.role; return permission.role;
}); });
} }
} }
function isUser(req, res, next) { function isUser(req, res, next) {
if (req.params.shortName) { if (req.params.shortName) {
var short_name = req.params.shortName.toLowerCase(); var short_name = req.params.shortName.toLowerCase();
System.findOne({'shortName': short_name}, function(err, system) { System.findOne({ 'shortName': short_name }, function (err, system) {
if (err) { if (err) {
console.error('Error - System not found: ' + short_name); console.error('Error - System not found: ' + short_name);
res.status(505).send({ res.status(505).send({
success: false,
message: "System Not Found"
});
}
if (!system.private) return next();
// Looks like the system is private...
if (getRole(req) > 0) return next();
res.status(401).send({
success: false, success: false,
message: "System Not Found" message: "Insufficent Permission."
}); });
}
if (!system.private) return next();
// Looks like the system is private...
if (getRole(req) > 0) return next();
res.status(401).send({
success: false,
message: "Insufficent Permission."
}); });
});
} }
} }
@@ -100,13 +122,13 @@ configureExpress(app, passport)
const connect = () => { const connect = () => {
mongoose.connect(secrets.db, (err, res) => { mongoose.connect(secrets.db, (err, res) => {
if (err) { if (err) {
console.log(`Error connecting to ${secrets.db}. ${err}`) console.log(`Error connecting to ${secrets.db}. ${err}`)
} else { } else {
console.log(`Successfully connected to ${secrets.db}.`) console.log(`Successfully connected to ${secrets.db}.`)
} }
}) })
} }
connect() connect()
@@ -115,7 +137,7 @@ mongoose.connection.on("disconnected", connect)
var upload = multer({ var upload = multer({
dest: config.uploadDirectory dest: config.uploadDirectory
}); });
var clients = []; var clients = [];
@@ -123,7 +145,7 @@ var clients = [];
function addSystemClients(req, res, next) { function addSystemClients(req, res, next) {
var systemClients = {}; var systemClients = {};
for (var key in clients) { for (var key in clients) {
if (clients.hasOwnProperty(key)) { if (clients.hasOwnProperty(key)) {
const client = clients[key]; const client = clients[key];
const shortName = client.shortName; const shortName = client.shortName;
if (client.active) { if (client.active) {
@@ -150,14 +172,27 @@ app.get('/:shortName/calls/newer', calls.get_newer_calls);
app.get('/:shortName/calls/older', calls.get_older_calls); app.get('/:shortName/calls/older', calls.get_older_calls);
app.get('/:shortName/calls/:time/older', calls.get_iphone_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);
app.post('/:shortName/star/:id', calls.add_star, function(req,res) { app.post('/:shortName/star/:id', calls.add_star);
notify_clients(req.call);
});
/*------ UPLOADS ---------- upload.single('call'), uploads.upload,*/ /*------ UPLOADS ---------- upload.single('call'), uploads.upload,*/
app.post('/:shortName/upload', upload.single('call'), uploads.upload, function(req,res) { app.post('/:shortName/upload', upload.single('call'), uploads.upload, async function (req, res) {
notify_clients(req.call); notify_clients(req.call);
// This section is for sending out tweets when new calls come in
/*
if ((req.call.shortName == "dcfd") && (req.call.talkgroupNum == 101)) {
var call_time = new Date(req.call.time);
var call_link = "https://openmhz.com/system/dcfd?call-id=" + req.call._id + "&time=" + call_time.getTime();
var call_time_label = call_time.toLocaleDateString('en-US', { timeZone: "America/New_York" }) + " " + call_time.toLocaleTimeString('en-US', { timeZone: "America/New_York" })
var status = req.call.len + " sec transmission\n" + call_time_label + "\n" + call_link;
const response = await client.post('statuses/update', {
status: status,
});
//console.log("Tweet Response: " + response);
}*/
}); });
/*------ SYSTEMS ----------*/ /*------ SYSTEMS ----------*/
@@ -171,41 +206,41 @@ app.get('/:shortName/groups', groups.get_groups);
/*------ STATS ----------*/ /*------ STATS ----------*/
app.get('/:shortName/errors',stats.get_errors); app.get('/:shortName/errors', stats.get_errors);
app.get('/:shortName/stats', stats.get_stats); app.get('/:shortName/stats', stats.get_stats);
function get_clients(req, res) { function get_clients(req, res) {
if (req.params.shortName) { if (req.params.shortName) {
var short_name = req.params.shortName.toLowerCase(); var short_name = req.params.shortName.toLowerCase();
} else { } else {
var short_name = null; var short_name = null;
} }
var total = 0; var total = 0;
var response = []; var response = [];
for (var key in clients) { for (var key in clients) {
if (clients.hasOwnProperty(key)) { if (clients.hasOwnProperty(key)) {
total++; total++;
if (!short_name || (clients[key].shortName == short_name)) { if (!short_name || (clients[key].shortName == short_name)) {
var age = (Date.now() - clients[key].timestamp) / 1000; var age = (Date.now() - clients[key].timestamp) / 1000;
var obj = { var obj = {
shortName: clients[key].shortName, shortName: clients[key].shortName,
filterCode: clients[key].filterCode, filterCode: clients[key].filterCode,
filterName: clients[key].filterName, filterName: clients[key].filterName,
filterType: clients[key].filterType, filterType: clients[key].filterType,
filterStarred: clients[key].filterStarred, filterStarred: clients[key].filterStarred,
active: clients[key].active, active: clients[key].active,
talkgroupNums: clients[key].talkgroupNums, talkgroupNums: clients[key].talkgroupNums,
timestamp: age timestamp: age
}
response.push(obj);
} }
response.push(obj);
} }
} }
response.push({"total": total}); }
response.push({ "total": total });
res.contentType('json'); res.contentType('json');
res.send(JSON.stringify(response)); res.send(JSON.stringify(response));
} }
/*------ CLIENTS ----------*/ /*------ CLIENTS ----------*/
@@ -214,75 +249,73 @@ 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("Caught an error");
console.error(err.stack); console.error(err.stack);
/* /*
res.status(err.status || 500); res.status(err.status || 500);
res.contentType('json'); res.contentType('json');
res.send(JSON.stringify({ res.send(JSON.stringify({
message: err.message, message: err.message,
error: err error: err
}));*/ }));*/
}); });
app.get('/:shortName/clients', get_clients); app.get('/:shortName/clients', get_clients);
app.get('/clients', get_clients); app.get('/clients', get_clients);
function notify_clients(call) { function notify_clients(call) {
call.type = "calls"; call.type = "calls";
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.shortName == call.shortName.toLowerCase()) {
// if client is not filtering for stars, or if the client is filtering and the call has stars // 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.filterStarred || call.star) {
if (client.filterCode == "") { if (client.filterCode == "") {
sent++; 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) {
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)); client.socket.emit("new message", JSON.stringify(call));
} else if (client.filterType == "unit") { sent++
var codeArray = client.filterCode.split(','); break;
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) {
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 (sent > 0) {
if (sent > 0) { //console.log("[" + call.shortName.toLowerCase() + "] Sent call to " + sent + " clients");
//console.log("[" + call.shortName.toLowerCase() + "] Sent call to " + sent + " clients"); }
}
} }
@@ -292,93 +325,90 @@ function notify_clients(call) {
io.sockets.on('connection', function (client) { io.sockets.on('connection', function (client) {
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', function (data) { client.on('start', function (data) {
if (clients[client.id] && data.shortName) { if (clients[client.id] && data.shortName) {
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);
clients[client.id].filterName = data.filterName; clients[client.id].filterName = data.filterName;
clients[client.id].filterStarred = data.filterStarred; clients[client.id].filterStarred = data.filterStarred;
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();
if ((data.filterType == "group") && (data.filterCode.indexOf(',') == -1)) { if ((data.filterType == "group") && (data.filterCode.indexOf(',') == -1)) {
db.get().collection("groups", function(err, groupCollection) { db.get().collection("groups", function (err, groupCollection) {
if (err || !groupCollection) { if (err || !groupCollection) {
console.error("Error - unable to open groups collection: " + err); console.error("Error - unable to open groups collection: " + err);
delete clients[client.id]; delete clients[client.id];
return; return;
}
if (!ObjectID.isValid(data.filterCode)) {
console.error("Error - Socket - Invalid Group ID: " + data.filterCode);
delete clients[client.id];
return;
}
groupCollection.findOne({
'shortName': data.shortName.toLowerCase(),
'_id': ObjectID.createFromHexString(data.filterCode)
}, function(err, group) {
if (err) {
console.warn("[" + data.shortName.toLowerCase() + "] Error - WebSocket Group ID not Found! Error: " + err + " Group ID: " + data.filterCode);
delete clients[client.id];
return;
} else {
if (group && clients[client.id]) {
clients[client.id].talkgroupNums = group.talkgroups;
} else {
console.error("Error - Socket: Invalid group or Client: " + data.filterCode + " Shortname: " + data.shortName + " ClientID: " + client.id);
delete clients[client.id];
return;
}
}
});
});
} if ((data.filterType == "talkgroup") && Array.isArray(data.filterCode)) {
clients[client.id].talkgroupNums = data.filterCode;
}
//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");
} }
}); if (!ObjectID.isValid(data.filterCode)) {
client.on('stop', function (data) { console.error("Error - Socket - Invalid Group ID: " + data.filterCode);
if (clients[client.id] ) { delete clients[client.id];
clients[client.id].active = false; return;
} else{ }
console.error("Error - Socket.io [Stop] either client not found "); groupCollection.findOne({
'shortName': data.shortName.toLowerCase(),
'_id': ObjectID.createFromHexString(data.filterCode)
}, function (err, group) {
if (err) {
console.warn("[" + data.shortName.toLowerCase() + "] Error - WebSocket Group ID not Found! Error: " + err + " Group ID: " + data.filterCode);
delete clients[client.id];
return;
} else {
if (group && clients[client.id]) {
clients[client.id].talkgroupNums = group.talkgroups;
} else {
console.error("Error - Socket: Invalid group or Client: " + data.filterCode + " Shortname: " + data.shortName + " ClientID: " + client.id);
delete clients[client.id];
return;
}
}
});
});
} if ((data.filterType == "talkgroup") && Array.isArray(data.filterCode)) {
clients[client.id].talkgroupNums = data.filterCode;
} }
}); //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);
client.on('disconnect', function() { } else {
delete clients[client.id]; console.error("Error - Socket.io [Start] either client not found or no Short Name");
}); }
});
client.on('stop', function (data) {
if (clients[client.id]) {
clients[client.id].active = false;
} else {
console.error("Error - Socket.io [Stop] either client not found ");
}
});
client.on('disconnect', function () {
delete clients[client.id];
});
}); });
//console.log("stats: " + util.inspect(call_stats)); //console.log("stats: " + util.inspect(call_stats));
// Connect to Mongo on start // Connect to Mongo on start
db.connect(function(err) { db.connect(function (err) {
if (err) { if (err) {
console.log('Unable to connect to Mongo.') console.log('Unable to connect to Mongo.')
process.exit(1) process.exit(1)
} else { } else {
stats.init_stats(); stats.init_stats();
server.listen(app.get("port"), (err) => { server.listen(app.get("port"), (err) => {
if (err) { if (err) {
console.err(err.stack) console.err(err.stack)
} else { } else {
console.log(`App listening on port ${app.get("port")} [${process.env.NODE_ENV} mode]`) console.log(`App listening on port ${app.get("port")} [${process.env.NODE_ENV} mode]`)
} }
}) })
} }
}) })
/* /*

View File

@@ -14,6 +14,7 @@ var systemSchema = mongoose.Schema({
showScreenName: Boolean, showScreenName: Boolean,
callAvg: Number, callAvg: Number,
callCount: Number, callCount: Number,
ignoreUnknownTalkgroup : Boolean,
active: {type: Boolean, default: false}, active: {type: Boolean, default: false},
planType: {type: Number, default: 0}, planType: {type: Number, default: 0},
userId: {type: mongoose.Schema.Types.ObjectId, ref: 'User'}, userId: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},

View File

@@ -1,5 +1,4 @@
var mongoose = require('mongoose'); var mongoose = require('mongoose');
var bcrypt = require('bcrypt-nodejs');
var talkgroupSchema = mongoose.Schema({ var talkgroupSchema = mongoose.Schema({
shortName: String, shortName: String,

2313
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,7 +20,7 @@
"get-folder-size": "^1.0.0", "get-folder-size": "^1.0.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"mongodb": "2.2.36", "mongodb": "2.2.36",
"mongoose": "5.3.12", "mongoose": "5.12.3",
"multer": "1.3.0", "multer": "1.3.0",
"node-ffprobe": "github:ListenerApproved/node-ffprobe", "node-ffprobe": "github:ListenerApproved/node-ffprobe",
"node-schedule": "1.2.4", "node-schedule": "1.2.4",

File diff suppressed because it is too large Load Diff

View File

@@ -65,6 +65,10 @@ services:
- S3_ENDPOINT - S3_ENDPOINT
- S3_BUCKET - S3_BUCKET
- S3_PROFILE - S3_PROFILE
- TWITTER_CONSUMER_KEY
- TWITTER_CONSUMER_SECRET
- TWITTER_ACCESS_TOKEN_KEY
- TWITTER_ACCESS_TOKEN_SECRET
volumes: volumes:
- "~/.aws:/home/app/.aws" - "~/.aws:/home/app/.aws"
depends_on: depends_on:
@@ -153,6 +157,7 @@ services:
- REACT_APP_BACKEND_SERVER - REACT_APP_BACKEND_SERVER
- REACT_APP_FRONTEND_SERVER - REACT_APP_FRONTEND_SERVER
- REACT_APP_SITE_NAME - REACT_APP_SITE_NAME
- REACT_APP_GOOGLE_ANALYTICS
- NODE_ENV - NODE_ENV
image: "frontend:${TAG}-${STAGE}" image: "frontend:${TAG}-${STAGE}"
environment: environment:
@@ -160,6 +165,7 @@ services:
- REACT_APP_BACKEND_SERVER - REACT_APP_BACKEND_SERVER
- REACT_APP_FRONTEND_SERVER - REACT_APP_FRONTEND_SERVER
- REACT_APP_SITE_NAME - REACT_APP_SITE_NAME
- REACT_APP_GOOGLE_ANALYTICS
depends_on: depends_on:
- mongo - mongo
networks: networks:

View File

@@ -5,6 +5,7 @@ ENV PATH /app/node_modules/.bin:$PATH
ARG REACT_APP_BACKEND_SERVER ARG REACT_APP_BACKEND_SERVER
ARG REACT_APP_SITE_NAME ARG REACT_APP_SITE_NAME
ARG REACT_APP_FRONTEND_SERVER ARG REACT_APP_FRONTEND_SERVER
ARG REACT_APP_GOOGLE_ANALYTICS
ARG NODE_ENV ARG NODE_ENV
COPY package.json ./ COPY package.json ./
RUN npm install RUN npm install

View File

@@ -3,27 +3,27 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@artsy/fresnel": "1.3.1", "@artsy/fresnel": "6.1.0",
"@reduxjs/toolkit": "^1.1.0", "@reduxjs/toolkit": "^1.9.1",
"@testing-library/jest-dom": "^4.2.4", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^9.3.2", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^7.1.2", "@testing-library/user-event": "^14.4.3",
"axios": "0.21.1", "axios": "1.2.1",
"connect-mongo": "3.2.0", "connected-react-router": "^6.9.3",
"connected-react-router": "6.8.0", "date-fns": "2.29.3",
"date-fns": "2.16.1", "ejs": "3.1.8",
"ejs": "3.1.6", "history": "^5.3.0",
"mongoose": "5.11.17", "react": "^18.2.0",
"react": "^17.0.1", "react-audio-player": "0.17.0",
"react-audio-player": "0.14.0", "react-datepicker": "4.8.0",
"react-datepicker": "3.3.0", "react-dom": "^18.2.0",
"react-dom": "^17.0.1", "react-ga": "^3.3.1",
"react-redux": "^7.1.3", "react-redux": "^8.0.5",
"react-router-dom": "5.2.0", "react-router-dom": "6.6.1",
"react-scripts": "4.0.1", "react-scripts": "5.0.1",
"redux-thunk": "2.3.0", "redux-thunk": "2.4.2",
"semantic-ui-css": "2.4.1", "semantic-ui-css": "2.5.0",
"semantic-ui-react": "2.0.1" "semantic-ui-react": "2.1.4"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",

View File

@@ -59,8 +59,8 @@ function getCard(req, res, next) {
function (err, item) { function (err, item) {
if (item) { if (item) {
var time = new Date(item.time); var time = new Date(item.time);
var timeString = time.toLocaleTimeString("en-US"); var timeString = time.toLocaleTimeString('en-US', {timeZone: "America/New_York"});
var dateString = time.toDateString(); var dateString = time.toLocaleDateString('en-US', {timeZone: "America/New_York"});
//console.log(item) //console.log(item)
res.render('card.ejs', { res.render('card.ejs', {
item: item, item: item,
@@ -135,15 +135,15 @@ async function getCalls(req, res, next) {
async function (err, item) { async function (err, item) {
if (item) { if (item) {
var time = new Date(item.time); var time = new Date(item.time);
var timeString = time.toLocaleTimeString("en-US"); var timeString = time.toLocaleTimeString('en-US', {timeZone: "America/New_York"});
var dateString = time.toDateString(); var dateString = time.toDateString('en-US', {timeZone: "America/New_York"});
//console.log(item) //console.log(item)
const tg_coll = db.get().collection('talkgroups'); const tg_coll = db.get().collection('talkgroups');
const tg = await tg_coll.findOne({ "num": item.talkgroupNum, 'shortName': req.params.shortName.toLowerCase()}) const tg = await tg_coll.findOne({ "num": item.talkgroupNum, 'shortName': req.params.shortName.toLowerCase()})
var title = item.len + " second transmission" var title = item.len + " second transmission"
if (tg) { if (tg) {
console.log(tg) //console.log(tg)
title = tg.description; title = tg.description;
} }
const callId = req.query["call-id"]; const callId = req.query["call-id"];

View File

@@ -0,0 +1,47 @@
import React, { Component } from "react";
import { Link } from 'react-router-dom'
import { Container, Header, Divider, Icon, Menu, Button } from "semantic-ui-react";
class AboutComponent extends Component {
render() {
return (
<div>
<Menu fixed="top">
<Link to="/"><Menu.Item link><Icon name='arrow left' /> Home</Menu.Item></Link>
<Link to="/systems"><Menu.Item link>Listen</Menu.Item></Link>
</Menu>
<Container text>
<Divider horizontal style={{ paddingTop: "4em", paddingBottom: "2em" }}><Header as="h1">About<Icon name='compass' /></Header></Divider>
<p>I wrote <a href="https://github.com/robotastic/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
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 recrodings.
I am hoping that making it easier to listen to what our local fire, police and EMS have to go through everyday will
lead to greater appreciation for all the work they do that goes largely unseen. I am personally amazed by the number
of medical runs the DC's Fire and EMS department has to cover every day, and the shocking number of them that are ODs.</p>
<p>Each system should have a 30 day archive. This is to help make sure things don't get overloaded and make sure I don't get a crazy bill
at the end of the month.</p>
<p> - Luke <a href="mailto:luke@robotastic.com?Subject=OpenMHz" target="_top">luke@robotastic.com</a> </p>
<Divider horizontal style={{ paddingTop: "4em", paddingBottom: "2em" }}><Header as="h2">Open Source</Header></Divider>
<p>Have changes you would like to suggest? Want to run your own server for your community? The source code for OpenMHz has been released:</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>
Support
</Button></a></p>
</Container>
</div>
);
}
}
export default AboutComponent;

View File

@@ -40,10 +40,8 @@ class CallPlayer extends React.Component {
this.addCall = this.addCall.bind(this); this.addCall = this.addCall.bind(this);
this.handlePusherClick = this.handlePusherClick.bind(this); this.handlePusherClick = this.handlePusherClick.bind(this);
this.handleSidebarToggle = this.handleSidebarToggle.bind(this); this.handleSidebarToggle = this.handleSidebarToggle.bind(this);
this.handleSupportToggle = this.handleSupportToggle.bind(this);
this.handleFilterClose = this.handleFilterClose.bind(this); this.handleFilterClose = this.handleFilterClose.bind(this);
this.handleGroupClose = this.handleGroupClose.bind(this); this.handleGroupClose = this.handleGroupClose.bind(this);
this.handleSupportClose = this.handleSupportClose.bind(this);
this.handleCalendarToggle = this.handleCalendarToggle.bind(this); this.handleCalendarToggle = this.handleCalendarToggle.bind(this);
this.handleCalendarClose = this.handleCalendarClose.bind(this); this.handleCalendarClose = this.handleCalendarClose.bind(this);
this.handleLiveToggle = this.handleLiveToggle.bind(this); this.handleLiveToggle = this.handleLiveToggle.bind(this);
@@ -61,7 +59,6 @@ class CallPlayer extends React.Component {
autoPlay: true, autoPlay: true,
callId: false, callId: false,
playTime: 0, playTime: 0,
sourceIndex: 0,
callScroll: false, callScroll: false,
callSelect: false, callSelect: false,
urlOptions: false, urlOptions: false,
@@ -69,8 +66,7 @@ class CallPlayer extends React.Component {
sidebarOpened: false, sidebarOpened: false,
filterVisible: false, filterVisible: false,
groupVisible: false, groupVisible: false,
calendarVisible: false, calendarVisible: false
supportVisible: false
} }
} }
@@ -92,10 +88,6 @@ class CallPlayer extends React.Component {
filterVisible: !this.state.filterVisible filterVisible: !this.state.filterVisible
}) })
handleSupportToggle = () => this.setState({
supportVisible: !this.state.supportVisible
})
handleContextRef = contextRef => this.setState({ contextRef }) handleContextRef = contextRef => this.setState({ contextRef })
changeUrl = url => this.props.callActions.changeUrl(url) changeUrl = url => this.props.callActions.changeUrl(url)
@@ -188,6 +180,9 @@ class CallPlayer extends React.Component {
} }
if (state.callId) { if (state.callId) {
if (search.length !== 1) {
search = search + '&';
}
search = search + `call-id=${state.callId}` search = search + `call-id=${state.callId}`
} }
if (props.filterDate) { if (props.filterDate) {
@@ -232,12 +227,6 @@ class CallPlayer extends React.Component {
this.props.callActions.fetchCalls(); this.props.callActions.fetchCalls();
} }
handleSupportClose() {
this.setState({
supportVisible: !this.state.supportVisible
});
}
handleFilterClose(didUpdate) { handleFilterClose(didUpdate) {
this.setState({ this.setState({
filterVisible: !this.state.filterVisible filterVisible: !this.state.filterVisible
@@ -256,7 +245,7 @@ class CallPlayer extends React.Component {
const nextCall = this.props.callsById[nextCallId]; const nextCall = this.props.callsById[nextCallId];
var callUrl = nextCall.url; var callUrl = nextCall.url;
this.setState({ callUrl: callUrl, callId: nextCallId, sourceIndex: 0, isPlaying: true }, () => { audio.playSource(callUrl) }); //scrollToComponent(this.currentCallRef.current); this.setState({ callUrl: callUrl, callId: nextCallId, isPlaying: true }, () => { audio.playSource(callUrl) }); //scrollToComponent(this.currentCallRef.current);
if (this.currentCallRef.current) { if (this.currentCallRef.current) {
this.currentCallRef.current.scrollIntoView({ this.currentCallRef.current.scrollIntoView({
behavior: "smooth", behavior: "smooth",
@@ -275,7 +264,6 @@ class CallPlayer extends React.Component {
this.setState({ this.setState({
callUrl: callUrl, callUrl: callUrl,
callId: data.call._id, callId: data.call._id,
sourceIndex: 0,
isPlaying: true isPlaying: true
}, () => { audio.playSource(callUrl); }); //scrollToComponent(this.currentCallRef.current); }, () => { audio.playSource(callUrl); }); //scrollToComponent(this.currentCallRef.current);
this.props.callActions.fetchCallInfo(data.call._id); this.props.callActions.fetchCallInfo(data.call._id);
@@ -342,7 +330,6 @@ class CallPlayer extends React.Component {
filter.live = false; filter.live = false;
this.setState({ this.setState({
callId: _id, callId: _id,
sourceIndex: 0,
urlOptions: true, urlOptions: true,
autoPlay: false, autoPlay: false,
callScroll: true, callScroll: true,
@@ -490,7 +477,6 @@ class CallPlayer extends React.Component {
var callUrl = call.url; var callUrl = call.url;
this.setState({ this.setState({
callUrl: callUrl, callUrl: callUrl,
sourceIndex: 0,
isPlaying: true, isPlaying: true,
callSelect: false callSelect: false
}, () => { audio.playSource(callUrl); }); //scrollToComponent(this.currentCallRef.current); }, () => { audio.playSource(callUrl); }); //scrollToComponent(this.currentCallRef.current);
@@ -548,7 +534,7 @@ class CallPlayer extends React.Component {
<FilterModal shortName={this.props.shortName} open={this.state.filterVisible} onClose={this.handleFilterClose} /> <FilterModal shortName={this.props.shortName} open={this.state.filterVisible} onClose={this.handleFilterClose} />
<CalendarModal open={this.state.calendarVisible} onClose={this.handleCalendarClose} archive={archive} key={this.props.shortName} /> <CalendarModal open={this.state.calendarVisible} onClose={this.handleCalendarClose} archive={archive} key={this.props.shortName} />
<GroupModal shortName={this.props.shortName} open={this.state.groupVisible} onClose={this.handleGroupClose} /> <GroupModal shortName={this.props.shortName} open={this.state.groupVisible} onClose={this.handleGroupClose} />
<SupportModal open={this.state.supportVisible} onClose={this.handleSupportClose} />
<Sidebar as={Menu} animation='overlay' inverted vertical visible={sidebarOpened} <Sidebar as={Menu} animation='overlay' inverted vertical visible={sidebarOpened}
onClick={this.handlePusherClick} duration={50} width='thin'> onClick={this.handlePusherClick} duration={50} width='thin'>
<Menu.Item onClick={this.handlePusherClick} > <Menu.Item onClick={this.handlePusherClick} >
@@ -612,7 +598,7 @@ class CallPlayer extends React.Component {
<Menu.Item active={this.state.autoPlay} onClick={() => this.switchAutoPlay()}><Icon name="level up" /><span className="desktop-only">Autoplay</span></Menu.Item> <Menu.Item active={this.state.autoPlay} onClick={() => this.switchAutoPlay()}><Icon name="level up" /><span className="desktop-only">Autoplay</span></Menu.Item>
<MediaPlayer ref={this.audioRef} call={currentCall} onEnded={this.callEnded} onPlayPause={this.handlePlayPause} /> <MediaPlayer ref={this.audioRef} call={currentCall} onEnded={this.callEnded} onPlayPause={this.handlePlayPause} />
<Menu.Menu position="right" className="desktop-only"> <Menu.Menu position="right" className="desktop-only">
<Menu.Item onClick={this.handleSupportToggle}><Icon name="coffee" />Support OpenMHz</Menu.Item> <Menu.Item><SupportModal/></Menu.Item>
<Menu.Item><a href={callDownload}><Icon name="download" />Download</a></Menu.Item> <Menu.Item><a href={callDownload}><Icon name="download" />Download</a></Menu.Item>
<Menu.Item><a href={callLink}><Icon name="at" />Link</a></Menu.Item> <Menu.Item><a href={callLink}><Icon name="at" />Link</a></Menu.Item>
</Menu.Menu> </Menu.Menu>

View File

@@ -5,8 +5,31 @@ import * as groupActionCreators from "../Group/group-actions"
import * as systemActionCreators from "../System/system-actions" import * as systemActionCreators from "../System/system-actions"
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
import CallPlayer from "./CallPlayer" import CallPlayer from "./CallPlayer"
import {
useLocation,
useNavigate,
useParams
} from "react-router-dom";
// glorious solution here: https://stackoverflow.com/questions/71097375/how-can-i-use-withrouter-in-react-router-v6-for-passing-props
function withRouter(Component) {
function ComponentWithRouterProp(props) {
let location = useLocation();
let navigate = useNavigate();
let params = useParams();
return (
<Component
{...props}
router={{ location, navigate, params }}
/>
);
}
return ComponentWithRouterProp;
}
function mapStateToProps(state, props) { function mapStateToProps(state, props) {
const shortName = props.router.params.shortName;
return { return {
callsById: state.call.byId, callsById: state.call.byId,
callsAllIds: state.call.allIds, callsAllIds: state.call.allIds,
@@ -18,14 +41,15 @@ function mapStateToProps(state, props) {
filterTalkgroups: state.call.filterTalkgroups, filterTalkgroups: state.call.filterTalkgroups,
filterGroupId: state.call.filterGroupId, filterGroupId: state.call.filterGroupId,
filterStarred: state.call.filterStarred, filterStarred: state.call.filterStarred,
system: state.system.items.find((item) => item.shortName === props.match.params.shortName), system: state.system.items.find((item) => item.shortName === shortName),
groups: state.group.items[props.match.params.shortName], groups: state.group.items[shortName],
talkgroups: state.talkgroup.items[props.match.params.shortName], talkgroups: state.talkgroup.items[shortName],
callsIsWaiting: state.call.isWaiting, callsIsWaiting: state.call.isWaiting,
systemIsWaiting: state.system.isWaiting, systemIsWaiting: state.system.isWaiting,
talkgroupsIsWaiting: state.talkgroup.isWaiting, talkgroupsIsWaiting: state.talkgroup.isWaiting,
groupsIsWaiting: state.group.isWaiting, groupsIsWaiting: state.group.isWaiting,
shortName: props.match.params.shortName location: props.router.location,
shortName: shortName
} }
} }
@@ -39,7 +63,7 @@ function mapDispatchToProps(dispatch) {
} }
export default connect( export default withRouter(connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(CallPlayer) )(CallPlayer))

View File

@@ -1,9 +1,9 @@
import React from "react"; import React from "react";
import { import {
Menu, Menu,
Icon, Icon,
Progress, Progress,
Label Label
} from "semantic-ui-react"; } from "semantic-ui-react";
import ReactAudioPlayer from 'react-audio-player' import ReactAudioPlayer from 'react-audio-player'
@@ -23,22 +23,22 @@ class MediaPlayer extends React.Component {
isPlaying: false isPlaying: false
} }
} }
handlePause = () => { this.setState({isPlaying: false}); this.props.onPlayPause(false); } handlePause = () => { this.setState({ isPlaying: false }); this.props.onPlayPause(false); }
handlePlay = () => { this.setState({isPlaying: true}); this.props.onPlayPause(true); } handlePlay = () => { this.setState({ isPlaying: true }); this.props.onPlayPause(true); }
playPause = () => {const audio = this.audioRef.current.audioEl.current; if (this.state.isPlaying) {audio.pause();} else {audio.play();}} playPause = () => { const audio = this.audioRef.current.audioEl.current; if (this.state.isPlaying) { audio.pause(); } else { audio.play(); } }
playSource(callUrl) { playSource(callUrl) {
const audio = this.audioRef.current.audioEl.current; const audio = this.audioRef.current.audioEl.current;
var onEnded = this.props.onEnded; var onEnded = this.props.onEnded;
audio.src=callUrl; audio.src = callUrl;
var playPromise = audio.play(); var playPromise = audio.play();
// In browsers that don’t yet support this functionality, // In browsers that don’t yet support this functionality,
// playPromise won’t be defined. // playPromise won’t be defined.
if (playPromise !== undefined) { if (playPromise !== undefined) {
playPromise.then(function() { playPromise.then(function () {
}).catch(function(error) { }).catch(function (error) {
console.log("Automatic playback failed: " + error); console.log("Automatic playback failed: " + error);
onEnded(); onEnded();
// Show a UI element to let the user manually start playback. // Show a UI element to let the user manually start playback.
@@ -48,7 +48,7 @@ class MediaPlayer extends React.Component {
updatePlayProgress() { updatePlayProgress() {
const audio = this.audioRef.current.audioEl.current; const audio = this.audioRef.current.audioEl.current;
const {currentTime, duration} = audio; const { currentTime, duration } = audio;
var call = this.props.call; var call = this.props.call;
// this checks to see if it should display the next Source ID // this checks to see if it should display the next Source ID
@@ -65,92 +65,60 @@ class MediaPlayer extends React.Component {
}); });
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (this.props.call && (prevProps.call !== this.props.call)) { if (this.props.call && (prevProps.call !== this.props.call)) {
this.setState({ this.setState({
sourceIndex: 0 sourceIndex: 0
}); });
}
}
componentDidUpdate() {
}
render(){
var playEnabled = { "disabled": true }
var sourceId = "-";
if (this.props.call) {
if (this.props.call.srcList.length > this.state.sourceIndex) {
sourceId = this.props.call.srcList[this.state.sourceIndex].src;
} }
playEnabled = {};
} }
return (
<Menu.Menu>
<ReactAudioPlayer
ref={this.audioRef}
onPause={this.handlePause}
onPlay={this.handlePlay}
listenInterval={500}
onListen={this.updatePlayProgress}
onEnded={this.props.onEnded}
autoPlay
/>
render() {
var playEnabled = { "disabled": true }
var sourceId = "-";
<Menu.Item onClick={this.playPause} > if (this.props.call) {
if (this.props.call.srcList.length > this.state.sourceIndex) {
{ sourceId = this.props.call.srcList[this.state.sourceIndex].src;
this.state.isPlaying
? (<Icon name="pause"/>)
: (<Icon name="play"/>)
}
</Menu.Item>
<Menu.Item>
<Progress inverted percent={this.state.playProgress}/>
<Label color="black">
{this.state.playTime}
Sec
</Label>
<Label color="black" className="desktop-only">
{sourceId}
</Label>
</Menu.Item>
</Menu.Menu>
/* <Media>
{
mediaProps => <Menu.Menu>
<Menu.Item onClick={() => mediaProps.playPause()} {...playEnabled} >
<Player src={callUrl} vendor="audio" onEnded={this.props.onEnded} autoPlay={true} onTimeUpdate={this.updatePlayProgress}/>
{
mediaProps.isPlaying
? (<Icon name="pause"/>)
: (<Icon name="play"/>)
}
</Menu.Item><Menu.Item>
<Progress inverted percent={this.state.playProgress}/>
<Label color="black">
{this.state.playTime}
Sec
</Label>
<Label color="black" className="desktop-only">
{sourceId}
</Label>
</Menu.Item>
</Menu.Menu>
} }
</Media>*/ playEnabled = {};
}
return (
<Menu.Menu>
<ReactAudioPlayer
ref={this.audioRef}
onPause={this.handlePause}
onPlay={this.handlePlay}
listenInterval={100}
onListen={this.updatePlayProgress}
onEnded={this.props.onEnded}
autoPlay
/>
)
} <Menu.Item onClick={this.playPause} >
{
this.state.isPlaying
? (<Icon name="pause" />)
: (<Icon name="play" />)
}
</Menu.Item>
<Menu.Item>
<Progress inverted percent={this.state.playProgress} />
<Label color="black">
{this.state.playTime}
Sec
</Label>
<Label color="black" className="desktop-only">
{sourceId}
</Label>
</Menu.Item>
</Menu.Menu>
)
}
} }
export default MediaPlayer; export default MediaPlayer;

View File

@@ -1,4 +1,4 @@
import React, { Component } from "react"; import {useState} from 'react';
import { import {
Modal, Modal,
Button, Button,
@@ -7,49 +7,35 @@ import {
import "./FilterModal.css"; import "./FilterModal.css";
class SupportModal extends Component { function SupportModal(props) {
constructor(props) {
super(props)
this.handleDone = this.handleDone.bind(this);
this.handleClose = this.handleClose.bind(this);
this.state = { const [open, setOpen] = useState(false);
open: false
}
}
const handleClose = () => props.onClose();
handleClose = () => this.props.onClose(false); return (
<Modal open={open} onClose={() => setOpen(false)} onOpen={() => setOpen(true)} trigger={<div><Icon name="coffee" />Support OpenMHz</div>} size='tiny' >
handleDone(event) { <Modal.Header>Support OpenMHz</Modal.Header>
this.props.onClose(); <Modal.Content image>
} <Icon size='massive' name="coffee" />
<Modal.Description>
render() { <p>If OpenMHz brings you joy, think about becoming a supporter! It will cover hosting costs and help keep me focused on development.</p>
return ( <p>No worries if not, I am not having problems keeping it going.</p>
<a href="https://github.com/sponsors/robotastic"><Button>
<Modal open={this.props.open} onClose={this.handleClose} size='tiny' >
<Modal.Header>Support OpenMHz</Modal.Header>
<Modal.Content image>
<Icon size='massive' name="coffee"/>
<Modal.Description>
<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>No worries if not, I am not having problems keeping it going.</p>
<a href="https://github.com/sponsors/robotastic"><Button>
Support Support
</Button></a> </Button></a>
</Modal.Description> </Modal.Description>
</Modal.Content> </Modal.Content>
<Modal.Actions> <Modal.Actions>
<Button onClick={this.handleDone} > <Button onClick={() => setOpen(false)} >
Done Done
</Button> </Button>
</Modal.Actions> </Modal.Actions>
</Modal> </Modal>
)
)
}
} }
export default SupportModal; export default SupportModal;

View File

@@ -0,0 +1,93 @@
// Import the RTK Query methods from the React-specific entry point
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
function buildCallUrl(state, date, direction) {
var params = {};
var url = "";
if ((typeof direction === 'string') && (typeof date === 'number')) {
url = url + '/' + direction;
params["time"] = date;
} else {
if (state.call.filterDate){
url = url + '/older'
params["time"] = state.call.filterDate;
}
}
if (state.call.filterStarred) {
params["filter-starred"] = true;
}
switch(state.call.filterType) {
case 1:
params["filter-type"] = "group";
params["filter-code"] = state.call.filterGroupId;
break;
case 2:
params["filter-type"] = "talkgroup";
params["filter-code"] = state.call.filterTalkgroups;
break;
default:
case 0:
}
/*
if (this.filterCode != "") {
params["filter-code"] = this.filterCode;
}
if (this.filterType == this.FilterType.Group) {
params["filter-type"] = "group";
params["filter-name"] = this.filterName;
}
if (this.filterType == this.FilterType.Talkgroup) {
params["filter-type"] = "talkgroup";
}
if (this.filterType == this.FilterType.Unit) {
params["filter-type"] = "unit";
}
*/
// url = startUp.backend_server + "/" + startUp.shortName.toLowerCase() + "/calls" + url + '?' + buildUrlParams(params);
url = process.env.REACT_APP_BACKEND_SERVER + "/" + state.call.shortName + "/calls" + url + '?' + buildUrlParams(params);
console.log("Trying to fetch data from this url: " + url);
return url;
}
// Define our single API slice object
export const apiSlice = createApi({
// The cache reducer expects to be added at `state.api` (already default - this is optional)
reducerPath: 'api',
// All of our requests will have URLs starting with '/fakeApi'
baseQuery: fetchBaseQuery({ baseUrl: process.env.REACT_APP_BACKEND_SERVER }),
// The "endpoints" represent operations and requests for this server
endpoints: builder => ({
// The `getPosts` endpoint is a "query" operation that returns data
getGroups: builder.query({
// The URL for the request is '/fakeApi/posts'
query: (shortName) => ({ url: '/${shortName}/groups' })
}),
getSystems: builder.query({
// The URL for the request is '/fakeApi/posts'
query: () => ({ url: '/systems' })
}),
getTalkgroups: builder.query({
// The URL for the request is '/fakeApi/posts'
query: (shortName) => ({ url: '/${shortName}/talkgroups' })
}),
getCalls: builder.query({
// The URL for the request is '/fakeApi/posts'
query: ({shortName, filterType, filterCode}) => ({
url: "/${shortName}/calls",
method: "GET",
params: { filterType, filterCode },
})
})
})
})
// Export the auto-generated hook for the `getPosts` query endpoint
export const { useGetGroupsQuery, useGetSystemsQuery, useGetTalkgroupsQuery, useGetCallsQuery } = apiSlice

View File

View File

@@ -0,0 +1,11 @@
const groupSlice = createSlice({
name: 'group',
initialState: [],
reducers: {
},
})
// Extract the action creators object and the reducer
const { actions, reducer } = groupSlice
// Export the reducer, either as a default or named export
export default reducer

View File

@@ -1,11 +1,15 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import { createRoot } from 'react-dom/client';
import { Routes ,Route } from 'react-router-dom';
import { BrowserRouter } from "react-router-dom";
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { Route, Switch } from 'react-router' // react-router v4/v5
import { ConnectedRouter } from 'connected-react-router'
import configureStore, { history } from './redux-router/configureStore' import configureStore, { history } from './redux-router/configureStore'
import 'semantic-ui-css/semantic.min.css' import 'semantic-ui-css/semantic.min.css'
// withTracker
//import withTracker from './withTracker';
// Main // Main
import MainContainer from "./Main/MainContainer" import MainContainer from "./Main/MainContainer"
@@ -15,20 +19,22 @@ import ListSystemsContainer from "./System/ListSystemsContainer"
// Call // Call
import CallPlayerContainer from "./Call/CallPlayerContainer" import CallPlayerContainer from "./Call/CallPlayerContainer"
const store = configureStore(/* provide initial state if any */) import AboutComponent from "./About/AboutComponent"
ReactDOM.render(
const store = configureStore(/* provide initial state if any */)
const container = document.getElementById('root');
const root = createRoot(container);
root.render(
<Provider store={store}> <Provider store={store}>
<ConnectedRouter history={history}> { /* place ConnectedRouter under Provider */ } <BrowserRouter>
<> { /* your usual react-router v4/v5 routing */ } <Routes>
<Switch> <Route exact path="/" element={<MainContainer/>} />
<Route exact path="/" component={MainContainer} /> <Route exact path="/systems" element={<ListSystemsContainer/>} />
<Route exact path="/systems" component={ListSystemsContainer} /> <Route exact path="/system/:shortName" element={<CallPlayerContainer/>} />
<Route exact path="/system/:shortName" component={CallPlayerContainer} /> <Route exact path="/about" element={<AboutComponent/>} />
<Route render={() => (<div>Miss</div>)} />
</Switch> </Routes>
</> </BrowserRouter>
</ConnectedRouter> </Provider>
</Provider>,
document.getElementById('root')
) )

View File

@@ -1,27 +1,21 @@
// https://github.com/supasate/connected-react-router // https://github.com/supasate/connected-react-router
// configureStore.js // configureStore.js
import { createBrowserHistory } from 'history'
import { applyMiddleware, compose, createStore } from 'redux'
import { routerMiddleware } from 'connected-react-router'
import createRootReducer from './reducers'
import thunk from 'redux-thunk';
export const history = createBrowserHistory() import { apiSlice } from '../features/api/apiSlice'
export default function configureStore(preloadedState) { import { configureStore } from '@reduxjs/toolkit'
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
createRootReducer(history), // root reducer with router state export default function configureAppStore(preloadedState) {
preloadedState, const store = configureStore({
composeEnhancers( reducer: {
applyMiddleware( [apiSlice.reducerPath]: apiSlice.reducer
routerMiddleware(history), // for dispatching history actions },
thunk, middleware: (getDefaultMiddleware) =>
// ... other middlewares ... getDefaultMiddleware().concat(apiSlice.middleware),
), preloadedState
), })
)
return store return store
} }

View File

@@ -6,14 +6,13 @@ import systemReducer from "../System/system-reducer"
import talkgroupReducer from "../Talkgroups/talkgroup-reducer" import talkgroupReducer from "../Talkgroups/talkgroup-reducer"
import groupReducer from "../Group/group-reducer" import groupReducer from "../Group/group-reducer"
import callReducer from "../Call/call-reducer" import callReducer from "../Call/call-reducer"
import { apiSlice } from '../features/api/apiSlice'
const createRootReducer = (history) => combineReducers({ export default (history) => combineReducers({
router: connectRouter(history), router: connectRouter(history),
system: systemReducer, system: systemReducer,
talkgroup: talkgroupReducer, talkgroup: talkgroupReducer,
group: groupReducer, group: groupReducer,
call: callReducer call: callReducer,
[apiSlice.reducerPath]: apiSlice.reducer
}) })
export default createRootReducer

View File

@@ -0,0 +1,39 @@
import React, { Component } from 'react';
import ReactGA from 'react-ga';
export default function withTracker(WrappedComponent, options = {}) {
const trackPage = (page) => {
ReactGA.set({
page,
...options
});
ReactGA.pageview(page);
};
const HOC = class extends Component {
componentDidMount() {
const {
location: { pathname: page }
} = this.props;
trackPage(page);
}
// eslint-disable-next-line camelcase
UNSAFE_componentWillReceiveProps(nextProps) {
const {
location: { pathname: currentPage }
} = this.props;
const nextPage = nextProps.location.pathname;
if (currentPage !== nextPage) {
trackPage(nextPage);
}
}
render() {
return <WrappedComponent {...this.props} />;
}
};
return HOC;
}

File diff suppressed because it is too large Load Diff

View File

@@ -39,5 +39,19 @@ http {
text/xml application/xml application/xml+rss text/javascript; text/xml application/xml application/xml+rss text/javascript;
include /etc/nginx/conf.d/*.conf; include /etc/nginx/conf.d/*.conf;
server {
# Enable NGINX status module
location /nginx_status {
# freely available with open source NGINX
stub_status;
access_log off;
# for open source NGINX < version 1.7.5
# stub_status on;
}
}
} }

6
node_modules/.yarn-integrity generated vendored
View File

@@ -1,8 +1,6 @@
{ {
"nodeVersion": "v10.15.3", "systemParams": "darwin-arm64-93",
"modulesFolders": [ "modulesFolders": [],
"node_modules"
],
"flags": [], "flags": [],
"linkedModules": [], "linkedModules": [],
"topLevelPatterns": [], "topLevelPatterns": [],

View File

@@ -22,3 +22,7 @@ export REACT_APP_ACCOUNT_SERVER="${PROTOCOL}account.${DOMAIN_NAME}"
export REACT_APP_ADMIN_SERVER="${PROTOCOL}admin.${DOMAIN_NAME}" export REACT_APP_ADMIN_SERVER="${PROTOCOL}admin.${DOMAIN_NAME}"
export REACT_APP_COOKIE_DOMAIN=".${DOMAIN_NAME}" export REACT_APP_COOKIE_DOMAIN=".${DOMAIN_NAME}"
export TWITTER_CONSUMER_KEY=""
export TWITTER_CONSUMER_SECRET=""
export TWITTER_ACCESS_TOKEN_KEY=""
export TWITTER_ACCESS_TOKEN_SECRET=""