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.
##
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`
@@ -144,6 +144,27 @@ From the Host OS run:
```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:
- **clean.js** This script removes all Calls that are over 30 days old

View File

@@ -47,6 +47,7 @@
"mongoose": "5.11.10",
"node-mailjet": "3.3.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 groupName = req.body.groupName.replace(/[^\w\s]/g, "");
var talkgroups = JSON.parse(req.body.talkgroups);
console.log(
"Talkgroups: " +
talkgroups +
" for group: " +
groupName +
" array: " +
Array.isArray(talkgroups)
);
if (!talkgroups.length || !Array.isArray(talkgroups)) {
message = "Talkgroups need to be selected, group can not be empty ";
}

View File

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

View File

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

View File

@@ -13,7 +13,8 @@ class SystemForm extends Component {
constructor(props) {
super(props);
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.checkInputs = this.checkInputs.bind(this);
this.render = this.render.bind(this);
@@ -30,6 +31,7 @@ class SystemForm extends Component {
county: "",
country: "",
showScreenName: false,
ignoreUnknownTalkgroup: false,
nameError: false,
shortNameError: false,
descriptionError: false,
@@ -51,6 +53,7 @@ class SystemForm extends Component {
this.state.county = this.props.system.county;
this.state.country = this.props.system.country;
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
@@ -66,6 +69,7 @@ class SystemForm extends Component {
county: this.props.system.county,
country: this.props.system.country,
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 });
handleCheckboxChange() {
handleScreenNameCheckboxChange() {
this.setState({ showScreenName: !this.state.showScreenName });
}
handleTalkgroupCheckboxChange() {
this.setState({ ignoreUnknownTalkgroup: !this.state.ignoreUnknownTalkgroup });
}
handleSubmit(event) {
event.preventDefault();
let inputError = this.checkInputs();
@@ -166,7 +173,8 @@ class SystemForm extends Component {
state,
county,
country,
showScreenName
showScreenName,
ignoreUnknownTalkgroup
}) => ({
name,
shortName,
@@ -176,7 +184,8 @@ class SystemForm extends Component {
state,
county,
country,
showScreenName
showScreenName,
ignoreUnknownTalkgroup
}))(this.state);
this.props.onSubmit(system);
}
@@ -294,13 +303,16 @@ class SystemForm extends Component {
</Form.Group>
<Divider horizontal />
<Form.Field>
<p>Your Screen Name: {this.props.screenName}</p>
<Checkbox
onChange={this.handleCheckboxChange}
onChange={this.handleScreenNameCheckboxChange}
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>
<Divider horizontal />
<Form.TextArea
@@ -386,19 +398,19 @@ class SystemForm extends Component {
/>
</Form.Field>
) : (
<Form.Field>
<Form.Dropdown
name="state"
onChange={this.handleInputChange}
error={this.state.stateError}
value={this.state.state}
options={stateOptions}
scrolling
label="State"
placeholder="Select State..."
/>
</Form.Field>
)}
<Form.Field>
<Form.Dropdown
name="state"
onChange={this.handleInputChange}
error={this.state.stateError}
value={this.state.state}
options={stateOptions}
scrolling
label="State"
placeholder="Select State..."
/>
</Form.Field>
)}
</Form.Group>

View File

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

View File

@@ -64,9 +64,11 @@ process.nextTick(function() {
var shortName = req.params.shortName.toLowerCase();
var apiKey = req.body.api_key;
System.findOne({
'shortName': shortName
}, async function(err, item) {
},["key", "ignoreUnknownTalkgroup"], async function(err, item) {
if (err) {
console.warn("[" + req.params.shortName + "] Error /:shortName/upload - Error: " + err);
@@ -96,8 +98,31 @@ process.nextTick(function() {
}
var startTime = req.body.start_time;
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
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.
var badFreqList = JSON.parse(req.body.freq_list);
var freqList = badFreqList.map(obj =>{
obj.error = obj.errors;
delete obj.errors;
return obj;

View File

@@ -32,6 +32,7 @@ const server = require('http').createServer(app);
const io = require('socket.io')(server, {});
var Twitter = require('twitter-lite');
io.origins('*:*');
@@ -43,50 +44,71 @@ io.origins((origin, callback) => {
callback(null, true);
});*/
io.on('connection', function(socket){
io.on('connection', function (socket) {
//console.log('a user connected');
socket.on('disconnect', function(){
socket.on('disconnect', function () {
//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) {
if (req.params.shortName && req.user) {
var short_name = req.params.shortName.toLowerCase();
var userId = req.user._id;
Permission.findOne({'userId': userId, 'shortName': short_name}, function(err, permission) {
if (err) {
console.error('Error - getRole userId: ' + userId + ' shortName: ' + short_name + ' error: ' + err);
return -1;
}
console.log('Found - getRole userId: ' + userId + ' shortName: ' + short_name + ' role: ' + permission.role);
var short_name = req.params.shortName.toLowerCase();
var userId = req.user._id;
Permission.findOne({ 'userId': userId, 'shortName': short_name }, function (err, permission) {
if (err) {
console.error('Error - getRole userId: ' + userId + ' shortName: ' + short_name + ' error: ' + err);
return -1;
}
console.log('Found - getRole userId: ' + userId + ' shortName: ' + short_name + ' role: ' + permission.role);
return permission.role;
});
return permission.role;
});
}
}
function isUser(req, res, next) {
if (req.params.shortName) {
var short_name = req.params.shortName.toLowerCase();
System.findOne({'shortName': short_name}, function(err, system) {
if (err) {
console.error('Error - System not found: ' + short_name);
res.status(505).send({
var short_name = req.params.shortName.toLowerCase();
System.findOne({ 'shortName': short_name }, function (err, system) {
if (err) {
console.error('Error - System not found: ' + short_name);
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,
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 = () => {
mongoose.connect(secrets.db, (err, res) => {
if (err) {
console.log(`Error connecting to ${secrets.db}. ${err}`)
} else {
console.log(`Successfully connected to ${secrets.db}.`)
}
})
mongoose.connect(secrets.db, (err, res) => {
if (err) {
console.log(`Error connecting to ${secrets.db}. ${err}`)
} else {
console.log(`Successfully connected to ${secrets.db}.`)
}
})
}
connect()
@@ -115,7 +137,7 @@ mongoose.connection.on("disconnected", connect)
var upload = multer({
dest: config.uploadDirectory
dest: config.uploadDirectory
});
var clients = [];
@@ -123,7 +145,7 @@ var clients = [];
function addSystemClients(req, res, next) {
var systemClients = {};
for (var key in clients) {
if (clients.hasOwnProperty(key)) {
if (clients.hasOwnProperty(key)) {
const client = clients[key];
const shortName = client.shortName;
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/:time/older', calls.get_iphone_calls);
app.get('/:shortName/calls', calls.get_calls);
app.post('/:shortName/star/:id', calls.add_star, function(req,res) {
notify_clients(req.call);
});
app.post('/:shortName/star/:id', calls.add_star);
/*------ 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);
// 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 ----------*/
@@ -171,41 +206,41 @@ app.get('/:shortName/groups', groups.get_groups);
/*------ STATS ----------*/
app.get('/:shortName/errors',stats.get_errors);
app.get('/:shortName/errors', stats.get_errors);
app.get('/:shortName/stats', stats.get_stats);
function get_clients(req, res) {
if (req.params.shortName) {
var short_name = req.params.shortName.toLowerCase();
} else {
var short_name = null;
}
var total = 0;
var response = [];
for (var key in clients) {
if (req.params.shortName) {
var short_name = req.params.shortName.toLowerCase();
} else {
var short_name = null;
}
var total = 0;
var response = [];
for (var key in clients) {
if (clients.hasOwnProperty(key)) {
total++;
if (!short_name || (clients[key].shortName == short_name)) {
var age = (Date.now() - clients[key].timestamp) / 1000;
var obj = {
shortName: clients[key].shortName,
filterCode: clients[key].filterCode,
filterName: clients[key].filterName,
filterType: clients[key].filterType,
filterStarred: clients[key].filterStarred,
active: clients[key].active,
talkgroupNums: clients[key].talkgroupNums,
timestamp: age
}
response.push(obj);
total++;
if (!short_name || (clients[key].shortName == short_name)) {
var age = (Date.now() - clients[key].timestamp) / 1000;
var obj = {
shortName: clients[key].shortName,
filterCode: clients[key].filterCode,
filterName: clients[key].filterName,
filterType: clients[key].filterType,
filterStarred: clients[key].filterStarred,
active: clients[key].active,
talkgroupNums: clients[key].talkgroupNums,
timestamp: age
}
response.push(obj);
}
}
response.push({"total": total});
}
response.push({ "total": total });
res.contentType('json');
res.send(JSON.stringify(response));
res.contentType('json');
res.send(JSON.stringify(response));
}
/*------ CLIENTS ----------*/
@@ -214,75 +249,73 @@ 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
}));*/
});
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
}));*/
});
app.get('/:shortName/clients', get_clients);
app.get('/clients', get_clients);
function notify_clients(call) {
call.type = "calls";
var sent = 0;
call.type = "calls";
var sent = 0;
for (var key in clients) {
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++;
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) {
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));
} 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));
sent++
break;
}
}
sent++
break;
}
}
}
}
}
}
}
}
}
if (sent > 0) {
//console.log("[" + call.shortName.toLowerCase() + "] Sent call to " + sent + " clients");
}
if (sent > 0) {
//console.log("[" + call.shortName.toLowerCase() + "] Sent call to " + sent + " clients");
}
}
@@ -292,93 +325,90 @@ function notify_clients(call) {
io.sockets.on('connection', function (client) {
clients[client.id] = {socket: client, active: false};
clients[client.id].timestamp = new Date();
client.on('start', function (data) {
if (clients[client.id] && data.shortName) {
clients[client.id].active = true;
clients[client.id].shortName = data.shortName.toLowerCase();
clients[client.id].filterCode = String(data.filterCode);
clients[client.id].filterName = data.filterName;
clients[client.id].filterStarred = data.filterStarred;
clients[client.id].filterType = String(data.filterType);
clients[client.id].talkgroupNums = [];
clients[client.id].timestamp = new Date();
if ((data.filterType == "group") && (data.filterCode.indexOf(',') == -1)) {
db.get().collection("groups", function(err, groupCollection) {
if (err || !groupCollection) {
console.error("Error - unable to open groups collection: " + err);
delete clients[client.id];
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");
clients[client.id] = { socket: client, active: false };
clients[client.id].timestamp = new Date();
client.on('start', function (data) {
if (clients[client.id] && data.shortName) {
clients[client.id].active = true;
clients[client.id].shortName = data.shortName.toLowerCase();
clients[client.id].filterCode = String(data.filterCode);
clients[client.id].filterName = data.filterName;
clients[client.id].filterStarred = data.filterStarred;
clients[client.id].filterType = String(data.filterType);
clients[client.id].talkgroupNums = [];
clients[client.id].timestamp = new Date();
if ((data.filterType == "group") && (data.filterCode.indexOf(',') == -1)) {
db.get().collection("groups", function (err, groupCollection) {
if (err || !groupCollection) {
console.error("Error - unable to open groups collection: " + err);
delete clients[client.id];
return;
}
});
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 ");
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;
}
});
client.on('disconnect', function() {
delete clients[client.id];
});
//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");
}
});
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));
// Connect to Mongo on start
db.connect(function(err) {
if (err) {
console.log('Unable to connect to Mongo.')
process.exit(1)
} else {
stats.init_stats();
db.connect(function (err) {
if (err) {
console.log('Unable to connect to Mongo.')
process.exit(1)
} else {
stats.init_stats();
server.listen(app.get("port"), (err) => {
if (err) {
console.err(err.stack)
} else {
console.log(`App listening on port ${app.get("port")} [${process.env.NODE_ENV} mode]`)
}
})
server.listen(app.get("port"), (err) => {
if (err) {
console.err(err.stack)
} else {
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,
callAvg: Number,
callCount: Number,
ignoreUnknownTalkgroup : Boolean,
active: {type: Boolean, default: false},
planType: {type: Number, default: 0},
userId: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},

View File

@@ -1,5 +1,4 @@
var mongoose = require('mongoose');
var bcrypt = require('bcrypt-nodejs');
var talkgroupSchema = mongoose.Schema({
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",
"mkdirp": "^0.5.1",
"mongodb": "2.2.36",
"mongoose": "5.3.12",
"mongoose": "5.12.3",
"multer": "1.3.0",
"node-ffprobe": "github:ListenerApproved/node-ffprobe",
"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_BUCKET
- S3_PROFILE
- TWITTER_CONSUMER_KEY
- TWITTER_CONSUMER_SECRET
- TWITTER_ACCESS_TOKEN_KEY
- TWITTER_ACCESS_TOKEN_SECRET
volumes:
- "~/.aws:/home/app/.aws"
depends_on:
@@ -153,6 +157,7 @@ services:
- REACT_APP_BACKEND_SERVER
- REACT_APP_FRONTEND_SERVER
- REACT_APP_SITE_NAME
- REACT_APP_GOOGLE_ANALYTICS
- NODE_ENV
image: "frontend:${TAG}-${STAGE}"
environment:
@@ -160,6 +165,7 @@ services:
- REACT_APP_BACKEND_SERVER
- REACT_APP_FRONTEND_SERVER
- REACT_APP_SITE_NAME
- REACT_APP_GOOGLE_ANALYTICS
depends_on:
- mongo
networks:

View File

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

View File

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

View File

@@ -59,8 +59,8 @@ function getCard(req, res, next) {
function (err, item) {
if (item) {
var time = new Date(item.time);
var timeString = time.toLocaleTimeString("en-US");
var dateString = time.toDateString();
var timeString = time.toLocaleTimeString('en-US', {timeZone: "America/New_York"});
var dateString = time.toLocaleDateString('en-US', {timeZone: "America/New_York"});
//console.log(item)
res.render('card.ejs', {
item: item,
@@ -135,15 +135,15 @@ async function getCalls(req, res, next) {
async function (err, item) {
if (item) {
var time = new Date(item.time);
var timeString = time.toLocaleTimeString("en-US");
var dateString = time.toDateString();
var timeString = time.toLocaleTimeString('en-US', {timeZone: "America/New_York"});
var dateString = time.toDateString('en-US', {timeZone: "America/New_York"});
//console.log(item)
const tg_coll = db.get().collection('talkgroups');
const tg = await tg_coll.findOne({ "num": item.talkgroupNum, 'shortName': req.params.shortName.toLowerCase()})
var title = item.len + " second transmission"
if (tg) {
console.log(tg)
//console.log(tg)
title = tg.description;
}
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.handlePusherClick = this.handlePusherClick.bind(this);
this.handleSidebarToggle = this.handleSidebarToggle.bind(this);
this.handleSupportToggle = this.handleSupportToggle.bind(this);
this.handleFilterClose = this.handleFilterClose.bind(this);
this.handleGroupClose = this.handleGroupClose.bind(this);
this.handleSupportClose = this.handleSupportClose.bind(this);
this.handleCalendarToggle = this.handleCalendarToggle.bind(this);
this.handleCalendarClose = this.handleCalendarClose.bind(this);
this.handleLiveToggle = this.handleLiveToggle.bind(this);
@@ -61,7 +59,6 @@ class CallPlayer extends React.Component {
autoPlay: true,
callId: false,
playTime: 0,
sourceIndex: 0,
callScroll: false,
callSelect: false,
urlOptions: false,
@@ -69,8 +66,7 @@ class CallPlayer extends React.Component {
sidebarOpened: false,
filterVisible: false,
groupVisible: false,
calendarVisible: false,
supportVisible: false
calendarVisible: false
}
}
@@ -92,10 +88,6 @@ class CallPlayer extends React.Component {
filterVisible: !this.state.filterVisible
})
handleSupportToggle = () => this.setState({
supportVisible: !this.state.supportVisible
})
handleContextRef = contextRef => this.setState({ contextRef })
changeUrl = url => this.props.callActions.changeUrl(url)
@@ -188,6 +180,9 @@ class CallPlayer extends React.Component {
}
if (state.callId) {
if (search.length !== 1) {
search = search + '&';
}
search = search + `call-id=${state.callId}`
}
if (props.filterDate) {
@@ -232,12 +227,6 @@ class CallPlayer extends React.Component {
this.props.callActions.fetchCalls();
}
handleSupportClose() {
this.setState({
supportVisible: !this.state.supportVisible
});
}
handleFilterClose(didUpdate) {
this.setState({
filterVisible: !this.state.filterVisible
@@ -256,7 +245,7 @@ class CallPlayer extends React.Component {
const nextCall = this.props.callsById[nextCallId];
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) {
this.currentCallRef.current.scrollIntoView({
behavior: "smooth",
@@ -275,7 +264,6 @@ class CallPlayer extends React.Component {
this.setState({
callUrl: callUrl,
callId: data.call._id,
sourceIndex: 0,
isPlaying: true
}, () => { audio.playSource(callUrl); }); //scrollToComponent(this.currentCallRef.current);
this.props.callActions.fetchCallInfo(data.call._id);
@@ -342,7 +330,6 @@ class CallPlayer extends React.Component {
filter.live = false;
this.setState({
callId: _id,
sourceIndex: 0,
urlOptions: true,
autoPlay: false,
callScroll: true,
@@ -490,7 +477,6 @@ class CallPlayer extends React.Component {
var callUrl = call.url;
this.setState({
callUrl: callUrl,
sourceIndex: 0,
isPlaying: true,
callSelect: false
}, () => { 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} />
<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} />
<SupportModal open={this.state.supportVisible} onClose={this.handleSupportClose} />
<Sidebar as={Menu} animation='overlay' inverted vertical visible={sidebarOpened}
onClick={this.handlePusherClick} duration={50} width='thin'>
<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>
<MediaPlayer ref={this.audioRef} call={currentCall} onEnded={this.callEnded} onPlayPause={this.handlePlayPause} />
<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={callLink}><Icon name="at" />Link</a></Menu.Item>
</Menu.Menu>

View File

@@ -5,8 +5,31 @@ import * as groupActionCreators from "../Group/group-actions"
import * as systemActionCreators from "../System/system-actions"
import { bindActionCreators } from 'redux'
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) {
const shortName = props.router.params.shortName;
return {
callsById: state.call.byId,
callsAllIds: state.call.allIds,
@@ -18,14 +41,15 @@ function mapStateToProps(state, props) {
filterTalkgroups: state.call.filterTalkgroups,
filterGroupId: state.call.filterGroupId,
filterStarred: state.call.filterStarred,
system: state.system.items.find((item) => item.shortName === props.match.params.shortName),
groups: state.group.items[props.match.params.shortName],
talkgroups: state.talkgroup.items[props.match.params.shortName],
system: state.system.items.find((item) => item.shortName === shortName),
groups: state.group.items[shortName],
talkgroups: state.talkgroup.items[shortName],
callsIsWaiting: state.call.isWaiting,
systemIsWaiting: state.system.isWaiting,
talkgroupsIsWaiting: state.talkgroup.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,
mapDispatchToProps
)(CallPlayer)
)(CallPlayer))

View File

@@ -1,9 +1,9 @@
import React from "react";
import {
Menu,
Icon,
Progress,
Label
Menu,
Icon,
Progress,
Label
} from "semantic-ui-react";
import ReactAudioPlayer from 'react-audio-player'
@@ -23,22 +23,22 @@ class MediaPlayer extends React.Component {
isPlaying: false
}
}
handlePause = () => { this.setState({isPlaying: false}); this.props.onPlayPause(false); }
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();}}
handlePause = () => { this.setState({ isPlaying: false }); this.props.onPlayPause(false); }
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(); } }
playSource(callUrl) {
const audio = this.audioRef.current.audioEl.current;
var onEnded = this.props.onEnded;
audio.src=callUrl;
audio.src = callUrl;
var playPromise = audio.play();
// In browsers that don’t yet support this functionality,
// playPromise won’t be defined.
if (playPromise !== undefined) {
playPromise.then(function() {
playPromise.then(function () {
}).catch(function(error) {
}).catch(function (error) {
console.log("Automatic playback failed: " + error);
onEnded();
// Show a UI element to let the user manually start playback.
@@ -48,7 +48,7 @@ class MediaPlayer extends React.Component {
updatePlayProgress() {
const audio = this.audioRef.current.audioEl.current;
const {currentTime, duration} = audio;
const { currentTime, duration } = audio;
var call = this.props.call;
// this checks to see if it should display the next Source ID
@@ -65,92 +65,60 @@ class MediaPlayer extends React.Component {
});
}
componentDidUpdate(prevProps) {
if (this.props.call && (prevProps.call !== this.props.call)) {
this.setState({
sourceIndex: 0
});
componentDidUpdate(prevProps) {
if (this.props.call && (prevProps.call !== this.props.call)) {
this.setState({
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} >
{
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>
if (this.props.call) {
if (this.props.call.srcList.length > this.state.sourceIndex) {
sourceId = this.props.call.srcList[this.state.sourceIndex].src;
}
</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;

View File

@@ -1,4 +1,4 @@
import React, { Component } from "react";
import {useState} from 'react';
import {
Modal,
Button,
@@ -7,49 +7,35 @@ import {
import "./FilterModal.css";
class SupportModal extends Component {
constructor(props) {
super(props)
this.handleDone = this.handleDone.bind(this);
this.handleClose = this.handleClose.bind(this);
function SupportModal(props) {
this.state = {
open: false
}
}
const [open, setOpen] = useState(false);
const handleClose = () => props.onClose();
handleClose = () => this.props.onClose(false);
handleDone(event) {
this.props.onClose();
}
render() {
return (
<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>
return (
<Modal open={open} onClose={() => setOpen(false)} onOpen={() => setOpen(true)} trigger={<div><Icon name="coffee" />Support OpenMHz</div>} 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
</Button></a>
</Modal.Description>
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button onClick={this.handleDone} >
Done
</Button>
</Modal.Actions>
</Modal>
</Modal.Content>
<Modal.Actions>
<Button onClick={() => setOpen(false)} >
Done
</Button>
</Modal.Actions>
</Modal>
)
)
}
}
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 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 { Route, Switch } from 'react-router' // react-router v4/v5
import { ConnectedRouter } from 'connected-react-router'
import configureStore, { history } from './redux-router/configureStore'
import 'semantic-ui-css/semantic.min.css'
// withTracker
//import withTracker from './withTracker';
// Main
import MainContainer from "./Main/MainContainer"
@@ -15,20 +19,22 @@ import ListSystemsContainer from "./System/ListSystemsContainer"
// Call
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}>
<ConnectedRouter history={history}> { /* place ConnectedRouter under Provider */ }
<> { /* your usual react-router v4/v5 routing */ }
<Switch>
<Route exact path="/" component={MainContainer} />
<Route exact path="/systems" component={ListSystemsContainer} />
<Route exact path="/system/:shortName" component={CallPlayerContainer} />
<Route render={() => (<div>Miss</div>)} />
</Switch>
</>
</ConnectedRouter>
</Provider>,
document.getElementById('root')
<BrowserRouter>
<Routes>
<Route exact path="/" element={<MainContainer/>} />
<Route exact path="/systems" element={<ListSystemsContainer/>} />
<Route exact path="/system/:shortName" element={<CallPlayerContainer/>} />
<Route exact path="/about" element={<AboutComponent/>} />
</Routes>
</BrowserRouter>
</Provider>
)

View File

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

View File

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

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;
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",
"modulesFolders": [
"node_modules"
],
"systemParams": "darwin-arm64-93",
"modulesFolders": [],
"flags": [],
"linkedModules": [],
"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_COOKIE_DOMAIN=".${DOMAIN_NAME}"
export TWITTER_CONSUMER_KEY=""
export TWITTER_CONSUMER_SECRET=""
export TWITTER_ACCESS_TOKEN_KEY=""
export TWITTER_ACCESS_TOKEN_SECRET=""