Я работал над этим в течение многих часов, и я не продолжаю решать эту проблему.
Весь мой код должен быть понятен, но для контекста у меня есть задание cron GAE, которое выглядит так: посмотрите, есть ли какие-либо заказы, в которых человек, отвечающий на звонок, положил трубку, и звонок не был завершен. Как вы можете видеть в отладчике, в этих предупреждениях нет документов (вы увидите это в коде), но по какой-то причине он все еще рассылает нам вызовы.
К вашему сведению, запланированные вызовы пн goose коллекция пуста.
const cron = require("node-cron");
const ScheduledCall = require("../models/scheduledCall.model");
const Order = require("../models/order.model");
const OrderConversation = require("../voice/models/OrderConversation.model");
const config = require("../voice/config");
const Restaurant = require("../models/restaurant.model");
const RestaurantMeta = require("../models/restaurantMeta.model");
const Killswitch = require("../models/killswitch");
const main = async () => {
let callsMade = 0;
let killed = await Killswitch.findOne({ name: "twilio" }).then(doc => {
return doc.toObject().killed;
});
if (!killed) {
ScheduledCall.find({
complete: false,
laterDateUnix: {
$gt: new Date().getTime() - 31000,
$lt: new Date().getTime() + 31000
}
})
.then(docs => {
return Promise.all(
docs.map(doc => {
callsMade++;
return twilio.calls
.create({
url: `.../api/voice/${doc.orderId}`,
to: `19802266478`,
from: "14242563710",
machineDetection: "Enable",
statusCallback: `.../api/completed/${order._id}`,
statusCallbackMethod: "POST"
})
.then(() =>
ScheduledCall.findByIdAndUpdate(doc._id, {
$set: { complete: true }
}).exec()
);
})
);
})
.then(() => {
return OrderConversation.find({
complete: false,
hungUp: true
}).exec();
})
.then(async docs => {
console.warn(`THIS IS DOCS ${JSON.stringify(docs)}`);
return await Promise.all(
docs.map(doc => {
callsMade++;
OrderConversation.findByIdAndUpdate(doc._id, {
$set: { responses: [], hungUp: false, reCall: true }
})
.exec()
.then(doc => {
return doc.toObject();
});
})
);
})
.then(conversations => {
console.warn(
`This is CONVERSATIONS : ${JSON.stringify(conversations)}`
);
return Promise.all(
conversations.map(c => {
return Order.findById(c.orderId)
.exec()
.then(doc => {
return doc.toObject();
});
})
);
})
.then(async orders => {
console.warn(`This is ORDERS : ${JSON.stringify(orders)}`);
return await Promise.all(
orders.map(order => {
RestaurantMeta.findOne({
urlSlug: order.restaurant
})
.exec()
.then(doc => {
return Restaurant.findById(doc.restaurantId).exec();
})
.then(doc => {
return doc.toObject();
})
.then(doc => {
console.warn(`THIS IS SUB DOC ${JSON.stringify(doc)}`);
return twilio.calls.create({
url: `.../api/voice/${order._id}`,
to: `1${doc.contact.phone}`,
from: "14242563710",
statusCallback: .../api/completed/${order._id}`,
statusCallbackMethod: "POST",
machineDetection: "Enable"
});
})
.catch(err => console.error(err));
})
);
})
.then(() => {
if (callsMade > 0) {
console.log(
`Finished cleaning up calls. Made a total of ${callsMade} calls.`
);
}
})
.catch(err => {
console.error("Error cleaning up calls. ", err);
});
}
};
module.exports = (req, res) => {
main();
setTimeout(function() {
main();
}, 10000);
setTimeout(function() {
main();
}, 20000);
setTimeout(function() {
main();
}, 30000);
setTimeout(function() {
main();
}, 40000);
setTimeout(function() {
main();
}, 50000);
setTimeout(function() {
return res.sendStatus(200);
}, 59999);
};
Это ^^ - задание cron, запущенное GAE.
var VoiceResponse = require("twilio").twiml.VoiceResponse;
const Order = require("../../models/order.model");
const formatCall = require("../helper/formatCall");
const orderConversation = require("../models/OrderConversation.model");
const funnyEndings = [
"..I like humans..",
"..If you see my friends Siri or Alexa, say hi to them for me!..",
"..Have you seen the movie terminator? It's my favorite..",
"..Someday I want to eat food myself, you know.."
];
// Main interview loop
module.exports = async function(request, response) {
var phone = request.body.From;
var input = request.body.RecordingUrl || request.body.Digits;
var answeredBy = request.body.answeredBy;
var twiml = new VoiceResponse();
let order;
try {
let orderData = await Order.findById(request.params.orderId).then(doc => {
return doc.toObject();
});
order = await formatCall(orderData, orderData.items);
} catch (err) {
console.error(err);
say("Terribly sorry, but an error has occurred. Goodbye.");
return respond();
}
if (answeredBy && answeredBy.toLowerCase() !== "human") {
return respond();
}
// helper to append a new "Say" verb with Polly.Salli voice
function say(text) {
twiml.say({ voice: "Polly.Salli" }, text);
}
// respond with the current TwiML content
function respond() {
response.type("text/xml");
response.send(twiml.toString());
}
// Find an in-progess order if one exists, otherwise create one
orderConversation.advanceSurvey(
{
phone: phone,
input: input,
order: order,
orderId: request.params.orderId,
hungUp: false
},
function(err, orderConversation, questionIndex) {
var question = order[questionIndex];
if (err || !orderConversation) {
}
// If question is null, we're done!
if (!question) {
say(
`Thank you for taking this order. ${
funnyEndings[
parseInt(Math.floor(Math.random() * funnyEndings.length))
]
} Well anyways goodbye!`
);
return respond();
}
// Add a greeting if this is the first question
if (questionIndex === 0 && !orderConversation.reCall) {
twiml.say(
{ voice: "Polly.Salli" },
"..This is the intro message that plays on the first call attempt"
);
} else if (orderConversation.reCall && questionIndex === 0) {
twiml.say(
{ voice: "Polly.Salli" },
"...This is the intro message that plays when we call the user back because they didn't finish the call"
);
}
// Otherwise, ask the next question
if (question.type !== "intro") {
say(question.text);
}
// Depending on the type of question, we either need to get input via
// DTMF tones or recorded speech
if (questionIndex === 0) {
const gather = twiml.gather({
timeout: 500,
numDigits: 1
});
gather.say({ voice: "Polly.Salli" }, question.text);
} else if (question.type === "choice" || question.type === "slow") {
twiml
.gather({
timeout: 500,
numDigits: 1
})
.say(
{ voice: "Polly.Salli" },
"Press 1 to move on, 2 to repeat, or 3 to go back"
);
} else if (question.type === "confirm") {
twiml
.gather({
timeout: 600,
finishOnKey: "#",
numDigits: 1
})
.say(
{ voice: "Polly.SallOkay i" },
"Press pound to confirm this order, or press any other key to go back to repeat the order summary."
);
}
// render TwiML response
respond();
}
);
};
Это ^^^ - это обработчик webhook, который мы передаем Twilio для вызова.
И, наконец,
var mongoose = require("mongoose");
mongoose.set("useFindAndModify", false);
// Define survey response model schema
var OrderConversationSchema = new mongoose.Schema({
// phone number of participant
phone: String,
// status of the participant's current survey response
complete: {
type: Boolean,
default: false
},
hungUp: { type: Boolean, default: false },
reCall: { type: Boolean, default: false },
orderId: String,
// record of answers
responses: [mongoose.Schema.Types.Mixed]
});
// For the given phone number and survey, advance the survey to the next
// question
OrderConversationSchema.statics.advanceSurvey = function(args, cb) {
var surveyData = args.order;
var phone = args.phone;
var input = args.input;
var orderId = args.orderId;
var orderConversation;
// Find current incomplete survey
OrderConversation.findOne(
{
orderId: orderId
},
async function(err, doc) {
if (doc) {
await OrderConversation.findByIdAndUpdate(doc._id, {
$set: { hungUp: false }
}).exec();
}
orderConversation =
doc ||
new OrderConversation({
phone: phone,
orderId: orderId
});
orderConversation.save().then(() => {
processInput();
});
}
);
// fill in any answer to the current question, and determine next question
// to ask
function processInput() {
// If we have input, use it to answer the current question
var responseLength = orderConversation.responses.length;
var currentQuestion = surveyData[responseLength];
// if there's a problem with the input, we can re-ask the same question
function reask() {
cb.call(orderConversation, null, orderConversation, responseLength);
}
// If we have no input, ask the current question again
if (input === undefined) return reask();
// Otherwise use the input to answer the current question
var questionResponse = {};
var choice;
if (currentQuestion.type === "intro") {
// Anything other than '1' or 'yes' is a false
questionResponse.answer = true;
choice = 1;
} else if (currentQuestion.type === "choice") {
// Try and cast to a Number
var num = Number(input);
if (isNaN(num)) {
// don't update the survey response, return the same question
return reask();
} else {
questionResponse.answer = num;
if (num == "1") {
choice = 1;
} else if (num == "2") {
choice = 0;
} else {
choice = -1;
}
}
} else {
// otherwise store raw value
questionResponse.answer = input;
}
// Save type from question
questionResponse.type = currentQuestion.type;
if (choice === 1) {
orderConversation.responses.push(questionResponse);
} else if (choice === -1) {
orderConversation.responses.pop();
}
// If new responses length is the length of survey, mark as done
if (orderConversation.responses.length === surveyData.length) {
orderConversation.complete = true;
}
// Save response
orderConversation.save(function(err) {
if (err) {
reask();
} else {
cb.call(
orderConversation,
err,
orderConversation,
responseLength + choice
);
}
});
}
};
// Export model
delete mongoose.models.OrderConversation;
delete mongoose.modelSchemas.OrderConversation;
var OrderConversation = mongoose.model(
"OrderConversation",
OrderConversationSchema
);
module.exports = OrderConversation;
Это ^^ - модель mon goose для управления разговорами заказов.