diff --git a/admin/server/controllers/systems.js b/admin/server/controllers/systems.js
index 190f1ca..6f3e9ee 100644
--- a/admin/server/controllers/systems.js
+++ b/admin/server/controllers/systems.js
@@ -123,6 +123,7 @@ exports.listSystems = async function (req, res, next) {
userId,
key,
showScreenName,
+ allowContact,
ignoreUnknownTalkgroup
}) => ({
name,
@@ -136,6 +137,7 @@ exports.listSystems = async function (req, res, next) {
userId,
key,
showScreenName,
+ allowContact,
ignoreUnknownTalkgroup
}))(obj);
if (obj.showScreenName) {
@@ -319,6 +321,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.allowContact = res.locals.allowContact;
res.locals.system.ignoreUnknownTalkgroup = res.locals.ignoreUnknownTalkgroup;
@@ -342,6 +345,7 @@ exports.updateSystem = async function (req, res, next) {
userId,
key,
showScreenName,
+ allowContact,
ignoreUnknownTalkgroup
}) => ({
name,
@@ -355,6 +359,7 @@ exports.updateSystem = async function (req, res, next) {
userId,
key,
showScreenName,
+ allowContact,
ignoreUnknownTalkgroup
}))(res.locals.system);
returnSys.id = res.locals.system._id;
@@ -506,6 +511,7 @@ exports.createSystem = async function (req, res, next) {
county,
country,
showScreenName,
+ allowContact,
ignoreUnknownTalkgroup
}) => ({
name,
@@ -517,6 +523,7 @@ exports.createSystem = async function (req, res, next) {
county,
country,
showScreenName,
+ allowContact,
ignoreUnknownTalkgroup
}))(res.locals);
system.key = key;
@@ -543,6 +550,7 @@ exports.createSystem = async function (req, res, next) {
country,
userId,
showScreenName,
+ allowContact,
ignoreUnknownTalkgroup,
key
}) => ({
@@ -556,6 +564,7 @@ exports.createSystem = async function (req, res, next) {
country,
userId,
showScreenName,
+ allowContact,
ignoreUnknownTalkgroup,
key
}))(newSys);
diff --git a/admin/server/models/system.js b/admin/server/models/system.js
index dc9fc8f..b19efe9 100644
--- a/admin/server/models/system.js
+++ b/admin/server/models/system.js
@@ -12,6 +12,10 @@ var systemSchema = mongoose.Schema({
state: String,
description: String,
showScreenName: Boolean,
+ allowContact: {
+ type: Boolean,
+ default: true
+ },
ignoreUnknownTalkgroup : Boolean,
active: {type: Boolean, default: false},
lastActive: Date,
diff --git a/admin/src/System/SystemForm.js b/admin/src/System/SystemForm.js
index 4b87991..e02b602 100644
--- a/admin/src/System/SystemForm.js
+++ b/admin/src/System/SystemForm.js
@@ -24,6 +24,7 @@ const SystemForm = (props) => {
const [county, setCounty] = useState("");
const [country, setCountry] = useState("");
const [showScreenName, setShowScreenName] = useState(false);
+ const [allowContact, setAllowContact] = useState(true);
const [ignoreUnknownTalkgroup, setIgnoreUnknownTalkgroup] = useState(false);
const [nameError, setNameError] = useState(false);
const [shortNameError, setShortNameError] = useState(false);
@@ -45,6 +46,7 @@ const SystemForm = (props) => {
setCounty(props.system.county);
setCountry(props.system.country);
setShowScreenName(props.system.showScreenName);
+ setAllowContact(props.system.allowContact);
setIgnoreUnknownTalkgroup(props.system.ignoreUnknownTalkgroup);
}
}, [props.system, props.isEditing]);
@@ -128,6 +130,12 @@ const SystemForm = (props) => {
return error;
}
+
+ const handleAllowContactCheckboxChange = () => {
+ setAllowContact((current) => !current);
+ }
+
+
const handleScreenNameCheckboxChange = () => {
setShowScreenName((current) => !current);
}
@@ -272,6 +280,12 @@ const SystemForm = (props) => {
checked={showScreenName}
label="Display Screen Name in System Listing" />
+
+
+
{
+ res.status(500);
+ res.json({
+ success: false,
+ message: err
+ });
+ return;
+ });
+ if (!system) {
+ console.error("[contact_system] The Short Name does not exist: " + req.params.shortName.toLowerCase());
+ res.status(500);
+ res.json({
+ success: false,
+ message: "That Short Name does not exist."
+ });
+ return;
+ }
+ if (!system.allowContact) {
+ console.error("[contact_system] This System does not allow user to contact the owner: " + req.params.shortName.toLowerCase());
+ res.status(500);
+ res.json({
+ success: false,
+ message: "This System does not allow user to contact the owner."
+ });
+ return;
+ }
+ const user = await User.findById(system.userId).exec();
+ if (!user) {
+ console.error("[contact_system] System User does not exist: " + req.params.shortName.toLowerCase());
+ res.status(500);
+ res.json({
+ success: false,
+ message: "System User does not exist."
+ });
+ return;
+ }
+
+ console.log(user);
+ let message = "Thank you for contributing a feed to OpenMHz. A user has sent in a message about this feed:\n\n-------------------------------\n"
+ message = message + "User Name: " + req.body.name + "\n";
+ message = message + "User Email: " + req.body.email + "\n-------------------------------\n";
+ message = message + "Message:\n" + req.body.message + "\n-------------------------------\n\n";
+ message = message + "If you wish to stop receiving User Messages for this System:\n - goto the Admin for " + site_name + ": " + admin_server + "\n - find this System\n - Update it and turn off this Allow Contact option."
+ const request = mailjet.post("send", {
+ version: "v3.1"
+ }).request({
+ Messages: [{
+ From: {
+ Email: admin_email,
+ Name: site_name + " Admin"
+ },
+ To: [{
+ Email: user.email,
+ Name: user.firstName + " " + user.lastName
+ },{
+ Email: admin_email,
+ Name: site_name + " Admin"
+ }],
+ Subject: site_name + " - " + system.shortName + " - " + " User Comment",
+ TextPart: message
+ }]
+ });
+ request.then(result => {
+ console.log(`User Comment sent to: ${user.email} for shortName: ${system.shortName}`)
+ })
+ .catch(err => {
+ console.error(err.statusCode);
+ console.error(err);
+ res.json({
+ success: false,
+ message: err
+ });
+ return;
+ });
+
+
+}
exports.get_systems = async function (req, res) {
let fromDate = new Date(Date.now() - 60 * 60 * 24 * 30 * 1000);
@@ -31,6 +122,7 @@ exports.get_systems = async function (req, res) {
lastActive: results[result].lastActive,
callAvg: results[result].callAvg,
description: results[result].description,
+ allowContact: results[result].allowContact,
clientCount: clientCount
}
if (results[result].showScreenName && results[result].userId) {
diff --git a/backend/index.js b/backend/index.js
index b167f77..5cd0877 100644
--- a/backend/index.js
+++ b/backend/index.js
@@ -116,6 +116,7 @@ app.post('/:shortName/upload', upload.single('call'), uploads.upload, async func
/*------ SYSTEMS ----------*/
app.get('/systems', addSystemClients, systems.get_systems);
+app.post('/:shortName/contact', systems.contact_system);
/*------ TALKGROUPS ----------*/
app.get('/:shortName/talkgroups', talkgroups.get_talkgroups);
@@ -249,6 +250,13 @@ io.sockets.on('connection', function (client) {
clients[client.id].filterType = String(data.filterType);
clients[client.id].talkgroupNums = [];
clients[client.id].timestamp = new Date();
+
+ if (typeof clients[client.id].filterCode != "string") {
+ console.error("Error - Socket - Invalid filterCode: " + data.filterCode + " ShortName: " + data.shortName.toLowerCase());
+ delete clients[client.id];
+ return;
+ }
+
if ((data.filterType == "group") && (data.filterCode.indexOf(',') == -1)) {
if (!ObjectId.isValid(data.filterCode)) {
console.error("Error - Socket - Invalid Group ID: " + data.filterCode);
diff --git a/backend/models/system.js b/backend/models/system.js
index 1ebaed4..a22127f 100644
--- a/backend/models/system.js
+++ b/backend/models/system.js
@@ -12,6 +12,10 @@ var systemSchema = mongoose.Schema({
state: String,
description: String,
showScreenName: Boolean,
+ allowContact: {
+ type: Boolean,
+ default: true
+ },
callAvg: Number,
callCount: Number,
ignoreUnknownTalkgroup : Boolean,
diff --git a/backend/package.json b/backend/package.json
index cb0ed81..580af82 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -22,6 +22,7 @@
"ffprobe-static": "3.1.0",
"find-remove": "4.1.0",
"get-folder-size": "^4.0.0",
+ "node-mailjet": "6.0.2",
"mkdirp": "^2.1.6",
"mongodb": "5.2.0",
"mongoose": "7.0.3",
diff --git a/docker-compose.yml b/docker-compose.yml
index e4d128a..157ee44 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -51,6 +51,10 @@ services:
pid: "host"
environment:
- MONGO_NODE_DRIVER_HOST=mongo
+ - MAILJET_KEY
+ - MAILJET_SECRET
+ - REACT_APP_ADMIN_EMAIL
+ - REACT_APP_SITE_NAME
- NODE_ENV
- REACT_APP_BACKEND_SERVER
- REACT_APP_ADMIN_SERVER
diff --git a/frontend/src/System/ContactModal.js b/frontend/src/System/ContactModal.js
new file mode 100644
index 0000000..7363bf8
--- /dev/null
+++ b/frontend/src/System/ContactModal.js
@@ -0,0 +1,87 @@
+import {useState} from 'react';
+import {
+ Modal,
+ Button,
+ Icon,
+ Label,
+ Form,
+ Grid
+} from "semantic-ui-react";
+import { useContactSystemMutation } from '../features/api/apiSlice'
+
+function ContactModal(props) {
+
+ const [contactSystem, { isLoading }] = useContactSystemMutation()
+ const {system, onClose} = props;
+ const [email, setEmail] = useState(false);
+ const [name, setName] = useState(false);
+ const [error, setError] = useState(false);
+ const [message, setMessage] = useState(false);
+
+ const handleSubmit = async () => {
+ if ((!name || name.length < 3) || (!email || email.length < 3) || (!message || message.length < 3)) {
+ setError("Please fill out your Name, Email and a Message with something useful");
+ } else {
+
+ const body = {
+ name,
+ email,
+ message
+ }
+ try {
+ const shortName = system.shortName;
+ const returned = await contactSystem({body,shortName}).unwrap();
+ console.log(returned);
+ setName(false);
+ setEmail(false);
+ setMessage(false);
+ //onClose();
+ } catch (submitError) {
+ console.error(error)
+ setError(submitError);
+ // you can handle errors here if you want to
+ }
+
+
+ }
+ }
+
+
+ return (
+
+ Contact Feed Contributer
+
+
+
+
+
+ Stuf about what to do
+
+
+
+ setName(e.target.value)}/>
+ setEmail(e.target.value)} />
+
+ setMessage(e.target.value)} />
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+
+}
+
+export default ContactModal;
diff --git a/frontend/src/System/InternationList.js b/frontend/src/System/InternationList.js
index 35138fd..f09e3cc 100644
--- a/frontend/src/System/InternationList.js
+++ b/frontend/src/System/InternationList.js
@@ -6,7 +6,7 @@ import { useNavigate } from 'react-router-dom'
import SystemCard from "./SystemCard";
-const InternationList = (other) => {
+const InternationList = (other, onContactClick) => {
const navigate = useNavigate();
let international = [];
@@ -15,7 +15,7 @@ const InternationList = (other) => {
{other &&
other.map((system) => {
- return navigate("/system/" + system.shortName)} />
+ return navigate("/system/" + system.shortName)} onContactClick={onContactClick} />
})}
))
diff --git a/frontend/src/System/ListSystems.js b/frontend/src/System/ListSystems.js
index 5b99cb2..0e46f36 100644
--- a/frontend/src/System/ListSystems.js
+++ b/frontend/src/System/ListSystems.js
@@ -1,6 +1,6 @@
-import { Component, useRef } from "react";
+import { Component, useRef, useState } from "react";
import { Link, useNavigate } from 'react-router-dom'
-
+import ContactModal from "./ContactModal";
import { useSelector, useDispatch } from 'react-redux'
import {
Container,
@@ -24,7 +24,8 @@ import InternationList from "./InternationList";
// ----------------------------------------------------
const ListSystems = (props) => {
-
+ const [contactVisible, setContactVisible] = useState(false);
+ const [contactSystem, setContactSystem] = useState(false);
const { data: systems, isSuccess } = useGetSystemsQuery();
//const systems = useSelector(selectActiveSystems)
const contextRef = useRef();
@@ -45,13 +46,19 @@ const ListSystems = (props) => {
}
}
+ const handleContactClick = (system) => {
+ setContactSystem(system);
+ setContactVisible(true);
+ };
+
const stateList = StateLinkList(states);
- const systemsByState = SystemsByState(states);
- const international = InternationList(other);
+ const systemsByState = SystemsByState(states, handleContactClick);
+ const international = InternationList(other, handleContactClick);
return (
+
setContactVisible(false)}/>
diff --git a/frontend/src/System/SystemCard.js b/frontend/src/System/SystemCard.js
index eb779a2..79e3051 100644
--- a/frontend/src/System/SystemCard.js
+++ b/frontend/src/System/SystemCard.js
@@ -1,4 +1,4 @@
-import React, { Component } from "react";
+import React, { Component, useState } from "react";
import {
Card,
Icon,
@@ -7,15 +7,26 @@ import {
} from "semantic-ui-react";
-class SystemCard extends Component {
-
- render() {
+function SystemCard (props) {
+
+ const {system, keepShort, onClick, onContactClick} = props;
var location = "";
- const system = this.props.system;
var clientCount = "";
var callAvg = "";
- var description = system.description;
- if (this.props.keepShort && (description.length > 100)) {
+ const description = system.description;
+
+
+
+ const handleContactClick = (event) => {
+ event.stopPropagation();
+ onContactClick(system);
+ }
+
+ const onCardClick = (event) => {
+ onClick();
+ }
+
+ if (keepShort && (description.length > 100)) {
description = description.substring(0, 96) + "...";
}
if (system.callAvg) {
@@ -75,7 +86,7 @@ class SystemCard extends Component {
}
}
return (
-
+
{system.name}
{location}
@@ -84,6 +95,7 @@ class SystemCard extends Component {
{system.screenName && }
+
@@ -99,7 +111,6 @@ class SystemCard extends Component {
);
- }
}
export default SystemCard;
diff --git a/frontend/src/System/SystemsByState.js b/frontend/src/System/SystemsByState.js
index 65a5629..5ef6b8e 100644
--- a/frontend/src/System/SystemsByState.js
+++ b/frontend/src/System/SystemsByState.js
@@ -6,7 +6,7 @@ import { useNavigate } from 'react-router-dom'
import SystemCard from "./SystemCard";
-const SystemsByState = (states) => {
+const SystemsByState = (states, onContactClick) => {
const navigate = useNavigate();
let systemsByState = [];
@@ -23,7 +23,7 @@ const SystemsByState = (states) => {
{states[state] &&
states[state].map((system) => {
- return navigate("/system/" + system.shortName)} />
+ return navigate("/system/" + system.shortName)} onContactClick={onContactClick} />
})}
))
diff --git a/frontend/src/features/api/apiSlice.js b/frontend/src/features/api/apiSlice.js
index c256cae..5e36386 100644
--- a/frontend/src/features/api/apiSlice.js
+++ b/frontend/src/features/api/apiSlice.js
@@ -54,10 +54,19 @@ export const apiSlice = createApi({
method: 'POST',
body: initialEvent
})
+ }),
+ contactSystem: builder.mutation({
+ query: (args) => {
+ const {shortName, body} = args;
+ return {
+ url: `/${shortName}/contact`,
+ method: 'POST',
+ body: body
+ }}
})
})
})
// Export the auto-generated hook for the `getPosts` query endpoint
-export const { useGetGroupsQuery, useGetSystemsQuery, useGetTalkgroupsQuery, useGetStatsQuery, useGetEventsQuery, useGetEventQuery, useAddNewEventMutation } = apiSlice
+export const { useGetGroupsQuery, useGetSystemsQuery, useGetTalkgroupsQuery, useGetStatsQuery, useGetEventsQuery, useGetEventQuery, useAddNewEventMutation, useContactSystemMutation } = apiSlice