From 3a7ec72aab1ad3411e67cdb4896facc6349f2077 Mon Sep 17 00:00:00 2001 From: Luke Berndt Date: Sun, 25 Jun 2023 09:59:48 -0400 Subject: [PATCH 1/2] Add support for sending messages --- admin/server/controllers/systems.js | 9 +++ admin/server/models/system.js | 4 ++ admin/src/System/SystemForm.js | 14 ++++ backend/controllers/systems.js | 92 ++++++++++++++++++++++++++ backend/index.js | 8 +++ backend/models/system.js | 4 ++ backend/package.json | 1 + docker-compose.yml | 4 ++ frontend/src/System/ContactModal.js | 87 ++++++++++++++++++++++++ frontend/src/System/InternationList.js | 4 +- frontend/src/System/ListSystems.js | 17 +++-- frontend/src/System/SystemCard.js | 29 +++++--- frontend/src/System/SystemsByState.js | 4 +- frontend/src/features/api/apiSlice.js | 11 ++- 14 files changed, 269 insertions(+), 19 deletions(-) create mode 100644 frontend/src/System/ContactModal.js 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)}/>
Radio Systems
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 From 16f74cdb17b6d7a5859712bcd5fdada04a79252c Mon Sep 17 00:00:00 2001 From: Luke Berndt Date: Sun, 9 Jul 2023 10:32:29 -0400 Subject: [PATCH 2/2] debugged --- admin/server/controllers/systems.js | 17 +++++++++++------ admin/src/System/SystemForm.js | 1 + backend/controllers/systems.js | 5 +++++ frontend/src/System/ContactModal.js | 16 +++++++++++++--- frontend/src/System/SystemCard.js | 2 +- 5 files changed, 31 insertions(+), 10 deletions(-) diff --git a/admin/server/controllers/systems.js b/admin/server/controllers/systems.js index 6f3e9ee..0eefb52 100644 --- a/admin/server/controllers/systems.js +++ b/admin/server/controllers/systems.js @@ -13,11 +13,11 @@ exports.isLoggedIn = function (req, res, next) { // ------------------------------------------- -exports.listUserSystems = async function (req,res,next) { +exports.listUserSystems = async function (req, res, next) { if (!req.user.admin) { res.status(401); res.json({ success: false, message: "Not Authorized" }); - return; + return; } const users = await User.find({}, "_id email firstName lastName lastLogin"); let list = []; @@ -27,7 +27,7 @@ exports.listUserSystems = async function (req,res,next) { const userId = new mongoose.Types.ObjectId(user._id); const systems = await System.find({ userId: userId }, "name shortName description systemType city state county country lastActive"); - user["systems"] = systems; + user["systems"] = systems; list.push(user); } res.json(list); @@ -36,9 +36,9 @@ exports.listUserSystems = async function (req,res,next) { exports.listAllSystems = async function (req, res, next) { if (!req.user.admin) { - res.status(401); - res.json({ success: false, message: "Not Authorized" }); - return; + res.status(401); + res.json({ success: false, message: "Not Authorized" }); + return; } const systems = await System.find().exec(); @@ -61,6 +61,7 @@ exports.listAllSystems = async function (req, res, next) { userId, key, showScreenName, + allowContact, ignoreUnknownTalkgroup, lastActive, active @@ -76,6 +77,7 @@ exports.listAllSystems = async function (req, res, next) { userId, key, showScreenName, + allowContact, ignoreUnknownTalkgroup, lastActive, active @@ -311,6 +313,8 @@ exports.uniqueShortName = async function (req, res, next) { // ------------------------------------------- +// All the values you want to use here need to be copied over in validateSystem() + exports.updateSystem = async function (req, res, next) { res.locals.system.name = res.locals.name; @@ -375,6 +379,7 @@ exports.validateSystem = async function (req, res, next) { try { res.locals.showScreenName = req.body.showScreenName; res.locals.ignoreUnknownTalkgroup = req.body.ignoreUnknownTalkgroup; + res.locals.allowContact = req.body.allowContact; if (!req.body.name || (req.body.name.length < 2)) { console.error("ERROR: Validate System - req.body.name"); res.status(500) diff --git a/admin/src/System/SystemForm.js b/admin/src/System/SystemForm.js index e02b602..73d29fc 100644 --- a/admin/src/System/SystemForm.js +++ b/admin/src/System/SystemForm.js @@ -158,6 +158,7 @@ const SystemForm = (props) => { county, country, showScreenName, + allowContact, ignoreUnknownTalkgroup }; props.onSubmit(system); diff --git a/backend/controllers/systems.js b/backend/controllers/systems.js index b9f6065..b709124 100644 --- a/backend/controllers/systems.js +++ b/backend/controllers/systems.js @@ -79,10 +79,14 @@ exports.contact_system = async function(req, res) { }); request.then(result => { console.log(`User Comment sent to: ${user.email} for shortName: ${system.shortName}`) + res.json({ + success: true + }); }) .catch(err => { console.error(err.statusCode); console.error(err); + res.status(500); res.json({ success: false, message: err @@ -97,6 +101,7 @@ exports.get_systems = async function (req, res) { let fromDate = new Date(Date.now() - 60 * 60 * 24 * 30 * 1000); const results = await System.find({lastActive: {$gte: fromDate}}).populate('userId', "screenName").catch( err => { console.error("Error - get_systems: " + err.message); + res.status(500); res.json({ success: false, message: err diff --git a/frontend/src/System/ContactModal.js b/frontend/src/System/ContactModal.js index 7363bf8..c606a8e 100644 --- a/frontend/src/System/ContactModal.js +++ b/frontend/src/System/ContactModal.js @@ -3,7 +3,7 @@ import { Modal, Button, Icon, - Label, + List, Form, Grid } from "semantic-ui-react"; @@ -35,7 +35,7 @@ function ContactModal(props) { setName(false); setEmail(false); setMessage(false); - //onClose(); + onClose(); } catch (submitError) { console.error(error) setError(submitError); @@ -55,7 +55,17 @@ function ContactModal(props) { - Stuf about what to do +

The radio systems on OpenMHz are contributed by people from around the world.

Use this form to contact the contributer for the {system.name} system regarding: + + Outages + Audio quality + Talkgroup updates + Thanks & appreciation + +

+

If you have general questions about OpenMHz send me an email,

+

- Luke luke@robotastic.com

+
diff --git a/frontend/src/System/SystemCard.js b/frontend/src/System/SystemCard.js index 79e3051..3504f3d 100644 --- a/frontend/src/System/SystemCard.js +++ b/frontend/src/System/SystemCard.js @@ -95,7 +95,7 @@ function SystemCard (props) { {system.screenName && } - + {/*system.allowContact && */}