SMS reminder app, Node/Twilio/Cron/Mongo: TypeError: Cannot read property 'utc' of undefined
SMS reminder app, Node/Twilio/Cron/Mongo: TypeError: Cannot read property 'utc' of undefined
我正在处理 Twilio 教程中的一些代码,一切似乎都运行良好,只是我没有收到任何通知。 Notifications Worker 运行后我收到此错误:
[grunt-develop] > (node:58755) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): TypeError: Cannot read property 'utc' of undefined
这是 appointment.js 文件,它似乎在 AppointmentSchema.methods 下导致错误:
var mongoose = require('mongoose');
var moment = require('moment');
var twilio = require('twilio');
var AppointmentSchema = new mongoose.Schema({
phoneNumber: String,
notification : String,
timeZone: String,
time : {type : Date, index : true}
});
AppointmentSchema.methods.requiresNotification = function (date) {
return Math.round(moment.duration(moment(this.time).tz(this.timeZone).utc()
.diff(moment(date).utc())
).asMinutes()) === this.notification;
};
AppointmentSchema.statics.sendNotifications = function(callback) {
// now
var searchDate = new Date();
Appointment
.find()
.then(function (appointments) {
appointments = appointments.filter(function(appointment) {
return appointment.requiresNotification(searchDate);
});
if (appointments.length > 0) {
sendNotifications(appointments);
}
});
// Send messages to all appoinment owners via Twilio
function sendNotifications(docs) {
var client = new twilio.RestClient(ACCOUNTSID, AUTHTOKEN);
docs.forEach(function(appointment) {
// Create options to send the message
var options = {
to: "+1" + appointment.phoneNumber,
from: '+17755834363',
body: "Just a reminder that you have an appointment coming up " + moment(appointment.time).calendar() +"."
};
// Send the message!
client.sendMessage(options, function(err, response) {
if (err) {
// Just log it for now
console.error(err);
} else {
// Log the last few digits of a phone number
var masked = appointment.phoneNumber.substr(0,
appointment.phoneNumber.length - 5);
masked += '*****';
console.log('Message sent to ' + masked);
}
});
});
// Don't wait on success/failure, just indicate all messages have been
// queued for delivery
if (callback) {
callback.call(this);
}
}
};
var Appointment = mongoose.model('appointment', AppointmentSchema);
module.exports = Appointment;
我不知道为什么它未定义,数据库中的一切似乎都正常显示。而且我不知道这是否是导致实际上未发送通知的原因。我已经为此努力了一段时间,如果有人有任何见解那就太好了。
文件结构:
root
├ config
| ├ auth.js
| ├ database.js
| ├ passport.js
├ controllers
| ├appointments.js
| ├routes.js
├ models
├appointment.js
├users.js
├ public
├ workers
├notificationsWorker
├ app.js
├ scheduler.js
通知的其他相关文件:
appointments.js
var momentTimeZone = require('moment-timezone');
var Appointment = require('../models/appointment');
var moment = require('moment');
module.exports = function(app, client) {
app.post('/user', function(req, res, next) {
var phoneNumber = req.body.phoneNumber;
var notification = req.body.notification;
var timeZone = req.body.timeZone;
var time = moment(req.body.time, "MM-DD-YYYY hh:mma");
var appointment = new Appointment({
phoneNumber: phoneNumber,
notification: notification,
timeZone: timeZone,
time: time
});
appointment.save()
.then(function () {
res.redirect('/user');
});
});
notificationsWorker.js
var Appointment = require('../models/appointment')
var notificationWorkerFactory = function(){
return {
run: function(){
Appointment.sendNotifications();
}
};
};
module.exports = notificationWorkerFactory();
scheduler.js
var CronJob = require('cron').CronJob;
var notificationsWorker = require('./workers/notificationsWorker');
var moment = require('moment');
var schedulerFactory = function(){
return {
start: function(){
new CronJob('00 * * * * *', function() {
console.log('Running Send Notifications Worker for ' + moment().format());
notificationsWorker.run();
}, null, true, '');
}
};
};
module.exports = schedulerFactory();
最后 app.js 文件:
const dotenv = require('dotenv').config({path: '.env'});
const exp = require('express');
const bodyParser = require('body-parser'); //body parser
const methodOverride = require('method-override'); //method override
const app = exp();
const session = require('express-session');
const PORT = process.env.PORT || 3000;
const fetchUrl = require('fetch').fetchUrl;
const request = require('request');
const sass = require('node-sass');
const exphbs = require('express-handlebars');
const favicon = require('serve-favicon');
const morgan = require('morgan');
const fs = require('fs');
const path = require('path');
const cookieParser = require('cookie-parser');
const mongoose = require('mongoose');
var appointments = require('./controllers/appointments');
var scheduler = require('./scheduler');
var ACCOUNTSID = process.env.TWILIO_ACCOUNT_ID;
var AUTHTOKEN = process.env.TWILIO_AUTH_TOKEN;
var twilio = require('twilio');
var client = new twilio.RestClient(ACCOUNTSID, AUTHTOKEN);
//databse stuff
const db = require('./config/database.js');
// mongoose.connect(db.url); // connect to our database
//passport
const passport = require('passport');
const flash = require('connect-flash');
app.use(session({ secret: 'blahblahblahbleck' })); // session secret
app.use(passport.initialize());
app.use(passport.session()); // persistent login sessions
app.use(flash()); // use connect-flash for flash messages stored in session
//views/middleware configs
app.engine('handlebars', exphbs({
layoutsDir: __dirname + '/views/layouts/',
defaultLayout: 'main',
partialsDir: [__dirname + '/views/partials/']
}));
app.set('view engine', 'handlebars');
app.set('views', __dirname + '/views');
app.locals.pretty = true
app.use('/', exp.static(__dirname + '/public'));
app.use('/bower_components', exp.static(__dirname + '/bower_components'));
app.use(methodOverride('_method')) //method override
app.use(bodyParser.urlencoded({
extended: false
// app.use(favicon(__dirname + '/public/imgages/favicon.ico'));
})); //body parser
app.use(bodyParser.json()); //body parser
app.use(cookieParser());
app.use(morgan('dev'));
app.locals.moment = require('moment');
require('./config/passport')(passport);
require('./controllers/routes')(app, passport);
require('./controllers/appointments')(app, client, db);
app.use('./controllers/appointments', appointments);
app.use('/', appointments);
// dynamically set controllers(routes)
fs.readdirSync('./controllers').forEach(function(file) {
routes = require('./controllers/' + file);
});
//start the server
app.listen(PORT, function() {
console.log('things that make you go hmmm on port ' + PORT);
});
scheduler.start();
module.exports = app;
==================已更新=================
我的表单输入视图:
<form class="omb_loginForm" action="/" autocomplete="off" method="POST">
<span class="help-block"></span>
<div class="input-group">
<span class="input-group-addon"></span>
<input type="text" class="form-control" name="phoneNumber" placeholder="phone number">
</div>
<span class="help-block"></span>
<div class="input-group">
<span class="input-group-addon"></span>
<input type="text" class="form-control" name="notification" placeholder="notification">
</div>
<span class="help-block"></span>
<div class="input-group">
<span class="input-group-addon"></span>
<select class="form-control" name="timeZone">
{{#each timeZone}}
<option>{{this}}</option>
{{/each}}
</select>
</div>
<span class="help-block"></span>
<div class="input-group date" id="datetimepicker1">
<input class="form-control" name="time">
<span class="input-group-addon glyphicon-calendar glyphicon">
</span>
</div>
<span class="help-block"></span>
<button class="btn btn-lg btn-primary btn-block" type="submit">
Submit
</button>
</form>
原始表单视图:
.form-group
label.col-sm-4.control-label(for='inputName') Name *
.col-sm-8
input#inputName.form-control(type='text', name='name', placeholder='Name', required='', data-parsley-maxlength='20', data-parsley-maxlength-message="This field can't have more than 20 characters", value="#{appointment.name}")
.form-group
label.col-sm-4.control-label(for='inputPhoneNumber') Phone Number
.col-sm-8
input#inputPhoneNumber.form-control(type='number', name='phoneNumber', placeholder='Phone Number', required='', value="#{appointment.phoneNumber}")
.form-group
label.col-sm-4.control-label(for='time') Appointment Date
.col-sm-8
input#inputDate.form-control(type='text', name='time', placeholder='Pick a Date', required='', value="#{moment(appointment.time).format('MM-DD-YYYY hh:mma')}")
.form-group
label.col-sm-4.control-label(for='selectNotification') Notification Time
.col-sm-8
select#selectDelta.form-control(name='notification', required='', value="#{appointment.notification}")
option(selected=appointment.notification == '', value='') Select a time
option(selected=appointment.notification == '15', value='15') 15 Minutes
option(selected=appointment.notification == '30', value='30') 30 Minutes
option(selected=appointment.notification == '45', value='45') 45 Minutes
option(selected=appointment.notification == '60', value='60') 60 Minutes
.form-group
label.col-sm-4.control-label(for='selectTimeZone') Time Zone
.col-sm-8
select#selectTimeZone.form-control(name='timeZone', required='', value="#{appointment.timeZone}")
each zone in timeZones
option()
option(selected=zone == appointment.timeZone, value="#{zone}") !{zone}
routes.js
app.get('/user', isLoggedIn, function(req, res) {
res.render('user', {
user : req.user,
timeZone: timeZones(),
appointment : new Appointment({
phoneNumber: "",
notification: '',
timeZone: "",
time:''}),
loggedIn: true, // get the user out of session and pass to template
layout: 'home'
});
});
通过更改 requiresNotification
中的逻辑,我终于能够让它工作。这是更新后的代码:
AppointmentSchema.methods.requiresNotification = function (date) {
var apptDate = moment.utc(this.time);
var current = moment.utc(date);
return Math.round(moment.duration(current.diff(apptDate))
.asMinutes()) === 0;
};
我正在查找约会时间和当前时间之间的时差。所以现在当 appointment date/time 和 current date/time 的分钟差为 0 时,发送通知.
我正在处理 Twilio 教程中的一些代码,一切似乎都运行良好,只是我没有收到任何通知。 Notifications Worker 运行后我收到此错误:
[grunt-develop] > (node:58755) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): TypeError: Cannot read property 'utc' of undefined
这是 appointment.js 文件,它似乎在 AppointmentSchema.methods 下导致错误:
var mongoose = require('mongoose');
var moment = require('moment');
var twilio = require('twilio');
var AppointmentSchema = new mongoose.Schema({
phoneNumber: String,
notification : String,
timeZone: String,
time : {type : Date, index : true}
});
AppointmentSchema.methods.requiresNotification = function (date) {
return Math.round(moment.duration(moment(this.time).tz(this.timeZone).utc()
.diff(moment(date).utc())
).asMinutes()) === this.notification;
};
AppointmentSchema.statics.sendNotifications = function(callback) {
// now
var searchDate = new Date();
Appointment
.find()
.then(function (appointments) {
appointments = appointments.filter(function(appointment) {
return appointment.requiresNotification(searchDate);
});
if (appointments.length > 0) {
sendNotifications(appointments);
}
});
// Send messages to all appoinment owners via Twilio
function sendNotifications(docs) {
var client = new twilio.RestClient(ACCOUNTSID, AUTHTOKEN);
docs.forEach(function(appointment) {
// Create options to send the message
var options = {
to: "+1" + appointment.phoneNumber,
from: '+17755834363',
body: "Just a reminder that you have an appointment coming up " + moment(appointment.time).calendar() +"."
};
// Send the message!
client.sendMessage(options, function(err, response) {
if (err) {
// Just log it for now
console.error(err);
} else {
// Log the last few digits of a phone number
var masked = appointment.phoneNumber.substr(0,
appointment.phoneNumber.length - 5);
masked += '*****';
console.log('Message sent to ' + masked);
}
});
});
// Don't wait on success/failure, just indicate all messages have been
// queued for delivery
if (callback) {
callback.call(this);
}
}
};
var Appointment = mongoose.model('appointment', AppointmentSchema);
module.exports = Appointment;
我不知道为什么它未定义,数据库中的一切似乎都正常显示。而且我不知道这是否是导致实际上未发送通知的原因。我已经为此努力了一段时间,如果有人有任何见解那就太好了。
文件结构:
root
├ config
| ├ auth.js
| ├ database.js
| ├ passport.js
├ controllers
| ├appointments.js
| ├routes.js
├ models
├appointment.js
├users.js
├ public
├ workers
├notificationsWorker
├ app.js
├ scheduler.js
通知的其他相关文件: appointments.js
var momentTimeZone = require('moment-timezone');
var Appointment = require('../models/appointment');
var moment = require('moment');
module.exports = function(app, client) {
app.post('/user', function(req, res, next) {
var phoneNumber = req.body.phoneNumber;
var notification = req.body.notification;
var timeZone = req.body.timeZone;
var time = moment(req.body.time, "MM-DD-YYYY hh:mma");
var appointment = new Appointment({
phoneNumber: phoneNumber,
notification: notification,
timeZone: timeZone,
time: time
});
appointment.save()
.then(function () {
res.redirect('/user');
});
});
notificationsWorker.js
var Appointment = require('../models/appointment')
var notificationWorkerFactory = function(){
return {
run: function(){
Appointment.sendNotifications();
}
};
};
module.exports = notificationWorkerFactory();
scheduler.js
var CronJob = require('cron').CronJob;
var notificationsWorker = require('./workers/notificationsWorker');
var moment = require('moment');
var schedulerFactory = function(){
return {
start: function(){
new CronJob('00 * * * * *', function() {
console.log('Running Send Notifications Worker for ' + moment().format());
notificationsWorker.run();
}, null, true, '');
}
};
};
module.exports = schedulerFactory();
最后 app.js 文件:
const dotenv = require('dotenv').config({path: '.env'});
const exp = require('express');
const bodyParser = require('body-parser'); //body parser
const methodOverride = require('method-override'); //method override
const app = exp();
const session = require('express-session');
const PORT = process.env.PORT || 3000;
const fetchUrl = require('fetch').fetchUrl;
const request = require('request');
const sass = require('node-sass');
const exphbs = require('express-handlebars');
const favicon = require('serve-favicon');
const morgan = require('morgan');
const fs = require('fs');
const path = require('path');
const cookieParser = require('cookie-parser');
const mongoose = require('mongoose');
var appointments = require('./controllers/appointments');
var scheduler = require('./scheduler');
var ACCOUNTSID = process.env.TWILIO_ACCOUNT_ID;
var AUTHTOKEN = process.env.TWILIO_AUTH_TOKEN;
var twilio = require('twilio');
var client = new twilio.RestClient(ACCOUNTSID, AUTHTOKEN);
//databse stuff
const db = require('./config/database.js');
// mongoose.connect(db.url); // connect to our database
//passport
const passport = require('passport');
const flash = require('connect-flash');
app.use(session({ secret: 'blahblahblahbleck' })); // session secret
app.use(passport.initialize());
app.use(passport.session()); // persistent login sessions
app.use(flash()); // use connect-flash for flash messages stored in session
//views/middleware configs
app.engine('handlebars', exphbs({
layoutsDir: __dirname + '/views/layouts/',
defaultLayout: 'main',
partialsDir: [__dirname + '/views/partials/']
}));
app.set('view engine', 'handlebars');
app.set('views', __dirname + '/views');
app.locals.pretty = true
app.use('/', exp.static(__dirname + '/public'));
app.use('/bower_components', exp.static(__dirname + '/bower_components'));
app.use(methodOverride('_method')) //method override
app.use(bodyParser.urlencoded({
extended: false
// app.use(favicon(__dirname + '/public/imgages/favicon.ico'));
})); //body parser
app.use(bodyParser.json()); //body parser
app.use(cookieParser());
app.use(morgan('dev'));
app.locals.moment = require('moment');
require('./config/passport')(passport);
require('./controllers/routes')(app, passport);
require('./controllers/appointments')(app, client, db);
app.use('./controllers/appointments', appointments);
app.use('/', appointments);
// dynamically set controllers(routes)
fs.readdirSync('./controllers').forEach(function(file) {
routes = require('./controllers/' + file);
});
//start the server
app.listen(PORT, function() {
console.log('things that make you go hmmm on port ' + PORT);
});
scheduler.start();
module.exports = app;
==================已更新=================
我的表单输入视图:
<form class="omb_loginForm" action="/" autocomplete="off" method="POST">
<span class="help-block"></span>
<div class="input-group">
<span class="input-group-addon"></span>
<input type="text" class="form-control" name="phoneNumber" placeholder="phone number">
</div>
<span class="help-block"></span>
<div class="input-group">
<span class="input-group-addon"></span>
<input type="text" class="form-control" name="notification" placeholder="notification">
</div>
<span class="help-block"></span>
<div class="input-group">
<span class="input-group-addon"></span>
<select class="form-control" name="timeZone">
{{#each timeZone}}
<option>{{this}}</option>
{{/each}}
</select>
</div>
<span class="help-block"></span>
<div class="input-group date" id="datetimepicker1">
<input class="form-control" name="time">
<span class="input-group-addon glyphicon-calendar glyphicon">
</span>
</div>
<span class="help-block"></span>
<button class="btn btn-lg btn-primary btn-block" type="submit">
Submit
</button>
</form>
原始表单视图:
.form-group
label.col-sm-4.control-label(for='inputName') Name *
.col-sm-8
input#inputName.form-control(type='text', name='name', placeholder='Name', required='', data-parsley-maxlength='20', data-parsley-maxlength-message="This field can't have more than 20 characters", value="#{appointment.name}")
.form-group
label.col-sm-4.control-label(for='inputPhoneNumber') Phone Number
.col-sm-8
input#inputPhoneNumber.form-control(type='number', name='phoneNumber', placeholder='Phone Number', required='', value="#{appointment.phoneNumber}")
.form-group
label.col-sm-4.control-label(for='time') Appointment Date
.col-sm-8
input#inputDate.form-control(type='text', name='time', placeholder='Pick a Date', required='', value="#{moment(appointment.time).format('MM-DD-YYYY hh:mma')}")
.form-group
label.col-sm-4.control-label(for='selectNotification') Notification Time
.col-sm-8
select#selectDelta.form-control(name='notification', required='', value="#{appointment.notification}")
option(selected=appointment.notification == '', value='') Select a time
option(selected=appointment.notification == '15', value='15') 15 Minutes
option(selected=appointment.notification == '30', value='30') 30 Minutes
option(selected=appointment.notification == '45', value='45') 45 Minutes
option(selected=appointment.notification == '60', value='60') 60 Minutes
.form-group
label.col-sm-4.control-label(for='selectTimeZone') Time Zone
.col-sm-8
select#selectTimeZone.form-control(name='timeZone', required='', value="#{appointment.timeZone}")
each zone in timeZones
option()
option(selected=zone == appointment.timeZone, value="#{zone}") !{zone}
routes.js
app.get('/user', isLoggedIn, function(req, res) {
res.render('user', {
user : req.user,
timeZone: timeZones(),
appointment : new Appointment({
phoneNumber: "",
notification: '',
timeZone: "",
time:''}),
loggedIn: true, // get the user out of session and pass to template
layout: 'home'
});
});
通过更改 requiresNotification
中的逻辑,我终于能够让它工作。这是更新后的代码:
AppointmentSchema.methods.requiresNotification = function (date) {
var apptDate = moment.utc(this.time);
var current = moment.utc(date);
return Math.round(moment.duration(current.diff(apptDate))
.asMinutes()) === 0;
};
我正在查找约会时间和当前时间之间的时差。所以现在当 appointment date/time 和 current date/time 的分钟差为 0 时,发送通知.