Add support for sending messages

This commit is contained in:
Luke Berndt
2023-06-25 09:59:48 -04:00
parent 72798e4494
commit 3a7ec72aab
14 changed files with 269 additions and 19 deletions

View File

@@ -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);

View File

@@ -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,

View File

@@ -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" />
</Form.Field>
<Form.Field>
<Checkbox
onChange={handleAllowContactCheckboxChange}
checked={allowContact}
label="Allow Users to Contact you via a Web Form. They will not get your email address unless you write them back." />
</Form.Field>
<Form.Field>
<Checkbox
onChange={handleTalkgroupCheckboxChange}

View File

@@ -1,6 +1,97 @@
var mongoose = require("mongoose");
var User = require("../models/user");
var System = require("../models/system");
const Mailjet = require('node-mailjet');
var admin_email = process.env['REACT_APP_ADMIN_EMAIL'] != null ? process.env['REACT_APP_ADMIN_EMAIL'] : "luke@openmhz.com";
var admin_server = process.env['REACT_APP_ADMIN_SERVER'] != null ? process.env['REACT_APP_ADMIN_SERVER'] : "https://admin.openmhz.com";
var site_name = process.env['REACT_APP_SITE_NAME'] != null ? process.env['REACT_APP_SITE_NAME'] : "OpenMHz";
const mailjet = new Mailjet({
apiKey: process.env['MAILJET_KEY'],
apiSecret: process.env['MAILJET_SECRET']
});
exports.contact_system = async function(req, res) {
var system = await System.findOne({
shortName: req.params.shortName.toLowerCase()
}).catch(err => {
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) {

View File

@@ -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);

View File

@@ -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,

View File

@@ -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",

View File

@@ -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

View File

@@ -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 (
<Modal open={props.open} onClose={onClose} size='large' >
<Modal.Header>Contact Feed Contributer</Modal.Header>
<Modal.Content>
<Modal.Description>
<Grid columns={2} divided>
<Grid.Row>
<Grid.Column width={6}>
Stuf about what to do
</Grid.Column>
<Grid.Column width={10}>
<Form>
<Form.Group widths='equal'>
<Form.Input fluid label='Name' placeholder='Name' onChange={e => setName(e.target.value)}/>
<Form.Input fluid label='Email' placeholder='Email' onChange={e => setEmail(e.target.value)} />
</Form.Group>
<Form.TextArea label='Message' placeholder='Provide feedback to the Feed Contributer' onChange={e => setMessage(e.target.value)} />
</Form>
</Grid.Column>
</Grid.Row>
</Grid>
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button onClick={(e) => onClose()} >
Cancel
</Button>
<Button onClick={(e) => {handleSubmit(); e.stopPropagation();}} >
Send
</Button>
</Modal.Actions>
</Modal>
)
}
export default ContactModal;

View File

@@ -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) => {
<Card.Group itemsPerRow={4} stackable={true}>
{other &&
other.map((system) => {
return <SystemCard system={system} key={system.shortName} onClick={(e) => navigate("/system/" + system.shortName)} />
return <SystemCard system={system} key={system.shortName} onClick={(e) => navigate("/system/" + system.shortName)} onContactClick={onContactClick} />
})}
</Card.Group>
))

View File

@@ -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 (
<div ref={contextRef}>
<NavBar />
<ContactModal system={contactSystem} open={contactVisible} onClose={()=> setContactVisible(false)}/>
<Container >
<Divider horizontal style={{ paddingTop: "5em", paddingBottom: "2em" }}><Header as="h1">Radio Systems<Icon name='rss' /></Header></Divider>
<Grid centered columns={2}>

View File

@@ -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 (
<Card link onClick={this.props.onClick} raised={true} centered={true} >
<Card link onClick={onCardClick} raised={true} centered={true} >
<Card.Content>
<Card.Header >{system.name}</Card.Header>
<Card.Meta>{location} </Card.Meta>
@@ -84,6 +95,7 @@ class SystemCard extends Component {
<Grid.Row>
<Grid.Column >
{system.screenName && <Label size="mini"><Icon name='user' /> {system.screenName}</Label>}
<Label size="mini" onClick={handleContactClick}><Icon name='mail' />Mail</Label>
</Grid.Column>
</Grid.Row>
<Grid.Row>
@@ -99,7 +111,6 @@ class SystemCard extends Component {
</Card.Content>
</Card>
);
}
}
export default SystemCard;

View File

@@ -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) => {
<Card.Group key={"group-" +state} itemsPerRow={4} stackable={true}>
{states[state] &&
states[state].map((system) => {
return <SystemCard system={system} key={system.shortName} onClick={(e) => navigate("/system/" + system.shortName)} />
return <SystemCard system={system} key={system.shortName} onClick={(e) => navigate("/system/" + system.shortName)} onContactClick={onContactClick} />
})}
</Card.Group>
))

View File

@@ -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