From ab004235f64be561af5e7e61cd59ed7b2dbac913 Mon Sep 17 00:00:00 2001 From: m5r Date: Mon, 30 Aug 2021 20:53:21 +0800 Subject: [PATCH] update outgoing call duration every 30 seconds until the call is over --- app/auth/mutations/logout.ts | 2 +- .../api/queue/insert-incoming-message.ts | 51 +----------- app/messages/api/queue/insert-messages.ts | 50 +----------- app/phone-calls/api/queue/insert-calls.ts | 38 +-------- .../api/queue/update-call-duration.ts | 27 +++++++ app/phone-calls/api/webhook/call.ts | 41 ++++------ app/public-area/pages/open-metrics.tsx | 2 +- integrations/twilio.ts | 77 +++++++++++++++++++ 8 files changed, 131 insertions(+), 157 deletions(-) create mode 100644 app/phone-calls/api/queue/update-call-duration.ts diff --git a/app/auth/mutations/logout.ts b/app/auth/mutations/logout.ts index 2ccc725..ac3a9af 100644 --- a/app/auth/mutations/logout.ts +++ b/app/auth/mutations/logout.ts @@ -1,5 +1,5 @@ import { Ctx } from "blitz"; -export default async function logout(_: any, ctx: Ctx) { +export default async function logout(_ = null, ctx: Ctx) { return await ctx.session.$revoke(); } diff --git a/app/messages/api/queue/insert-incoming-message.ts b/app/messages/api/queue/insert-incoming-message.ts index f90a65d..dd821b1 100644 --- a/app/messages/api/queue/insert-incoming-message.ts +++ b/app/messages/api/queue/insert-incoming-message.ts @@ -1,10 +1,10 @@ import { Queue } from "quirrel/blitz"; import type { MessageInstance } from "twilio/lib/rest/api/v2010/account/message"; -import db, { Direction, MessageStatus } from "../../../../db"; +import db from "../../../../db"; import { encrypt } from "../../../../db/_encryption"; import notifyIncomingMessageQueue from "./notify-incoming-message"; -import getTwilioClient from "../../../../integrations/twilio"; +import getTwilioClient, { translateMessageDirection, translateMessageStatus } from "../../../../integrations/twilio"; type Payload = { organizationId: string; @@ -31,8 +31,8 @@ const insertIncomingMessageQueue = Queue( id: messageSid, to: message.to, from: message.from, - status: translateStatus(message.status), - direction: translateDirection(message.direction), + status: translateMessageStatus(message.status), + direction: translateMessageDirection(message.direction), sentAt: message.dateCreated, content: encrypt(message.body, organization.encryptionKey), }, @@ -50,46 +50,3 @@ const insertIncomingMessageQueue = Queue( ); export default insertIncomingMessageQueue; - -function translateDirection(direction: MessageInstance["direction"]): Direction { - switch (direction) { - case "inbound": - return Direction.Inbound; - case "outbound-api": - case "outbound-call": - case "outbound-reply": - default: - return Direction.Outbound; - } -} - -function translateStatus(status: MessageInstance["status"]): MessageStatus { - switch (status) { - case "accepted": - return MessageStatus.Accepted; - case "canceled": - return MessageStatus.Canceled; - case "delivered": - return MessageStatus.Delivered; - case "failed": - return MessageStatus.Failed; - case "partially_delivered": - return MessageStatus.PartiallyDelivered; - case "queued": - return MessageStatus.Queued; - case "read": - return MessageStatus.Read; - case "received": - return MessageStatus.Received; - case "receiving": - return MessageStatus.Receiving; - case "scheduled": - return MessageStatus.Scheduled; - case "sending": - return MessageStatus.Sending; - case "sent": - return MessageStatus.Sent; - case "undelivered": - return MessageStatus.Undelivered; - } -} diff --git a/app/messages/api/queue/insert-messages.ts b/app/messages/api/queue/insert-messages.ts index bcac8ca..b71ad63 100644 --- a/app/messages/api/queue/insert-messages.ts +++ b/app/messages/api/queue/insert-messages.ts @@ -1,8 +1,9 @@ import { Queue } from "quirrel/blitz"; import type { MessageInstance } from "twilio/lib/rest/api/v2010/account/message"; -import db, { Direction, Message, MessageStatus } from "../../../../db"; +import db, { Message } from "../../../../db"; import { encrypt } from "../../../../db/_encryption"; +import { translateMessageDirection, translateMessageStatus } from "../../../../integrations/twilio"; type Payload = { organizationId: string; @@ -29,8 +30,8 @@ const insertMessagesQueue = Queue( content: encrypt(message.body, phoneNumber.organization.encryptionKey), from: message.from, to: message.to, - status: translateStatus(message.status), - direction: translateDirection(message.direction), + status: translateMessageStatus(message.status), + direction: translateMessageDirection(message.direction), sentAt: new Date(message.dateCreated), })) .sort((a, b) => a.sentAt.getTime() - b.sentAt.getTime()); @@ -40,46 +41,3 @@ const insertMessagesQueue = Queue( ); export default insertMessagesQueue; - -function translateDirection(direction: MessageInstance["direction"]): Direction { - switch (direction) { - case "inbound": - return Direction.Inbound; - case "outbound-api": - case "outbound-call": - case "outbound-reply": - default: - return Direction.Outbound; - } -} - -function translateStatus(status: MessageInstance["status"]): MessageStatus { - switch (status) { - case "accepted": - return MessageStatus.Accepted; - case "canceled": - return MessageStatus.Canceled; - case "delivered": - return MessageStatus.Delivered; - case "failed": - return MessageStatus.Failed; - case "partially_delivered": - return MessageStatus.PartiallyDelivered; - case "queued": - return MessageStatus.Queued; - case "read": - return MessageStatus.Read; - case "received": - return MessageStatus.Received; - case "receiving": - return MessageStatus.Receiving; - case "scheduled": - return MessageStatus.Scheduled; - case "sending": - return MessageStatus.Sending; - case "sent": - return MessageStatus.Sent; - case "undelivered": - return MessageStatus.Undelivered; - } -} diff --git a/app/phone-calls/api/queue/insert-calls.ts b/app/phone-calls/api/queue/insert-calls.ts index 32e4fb7..e3b9e0e 100644 --- a/app/phone-calls/api/queue/insert-calls.ts +++ b/app/phone-calls/api/queue/insert-calls.ts @@ -1,7 +1,8 @@ import { Queue } from "quirrel/blitz"; import type { CallInstance } from "twilio/lib/rest/api/v2010/account/call"; -import db, { Direction, CallStatus } from "../../../../db"; +import db from "../../../../db"; +import { translateCallDirection, translateCallStatus } from "../../../../integrations/twilio"; type Payload = { organizationId: string; @@ -25,8 +26,8 @@ const insertCallsQueue = Queue("api/queue/insert-calls", async ({ calls id: call.sid, from: call.from, to: call.to, - direction: translateDirection(call.direction), - status: translateStatus(call.status), + direction: translateCallDirection(call.direction), + status: translateCallStatus(call.status), duration: call.duration, createdAt: new Date(call.dateCreated), })) @@ -36,34 +37,3 @@ const insertCallsQueue = Queue("api/queue/insert-calls", async ({ calls }); export default insertCallsQueue; - -function translateDirection(direction: CallInstance["direction"]): Direction { - switch (direction) { - case "inbound": - return Direction.Inbound; - case "outbound": - default: - return Direction.Outbound; - } -} - -function translateStatus(status: CallInstance["status"]): CallStatus { - switch (status) { - case "busy": - return CallStatus.Busy; - case "canceled": - return CallStatus.Canceled; - case "completed": - return CallStatus.Completed; - case "failed": - return CallStatus.Failed; - case "in-progress": - return CallStatus.InProgress; - case "no-answer": - return CallStatus.NoAnswer; - case "queued": - return CallStatus.Queued; - case "ringing": - return CallStatus.Ringing; - } -} diff --git a/app/phone-calls/api/queue/update-call-duration.ts b/app/phone-calls/api/queue/update-call-duration.ts new file mode 100644 index 0000000..8ea71e3 --- /dev/null +++ b/app/phone-calls/api/queue/update-call-duration.ts @@ -0,0 +1,27 @@ +import { Queue } from "quirrel/blitz"; + +import db from "../../../../db"; +import getTwilioClient, { translateCallStatus } from "../../../../integrations/twilio"; + +type Payload = { + organizationId: string; + callId: string; +}; + +const updateCallDurationQueue = Queue("api/queue/update-call-duration", async ({ organizationId, callId }) => { + const organization = await db.organization.findFirst({ where: { id: organizationId } }); + const twilioClient = getTwilioClient(organization); + const call = await twilioClient.calls.get(callId).fetch(); + + await db.phoneCall.update({ + where: { id: callId }, + data: { duration: call.duration, status: translateCallStatus(call.status) }, + }); + + const callHasFinished = ["busy", "no-answer", "canceled", "failed"].includes(call.status); + if (!callHasFinished) { + await updateCallDurationQueue.enqueue({ organizationId, callId }, { delay: "30s" }); + } +}); + +export default updateCallDurationQueue; diff --git a/app/phone-calls/api/webhook/call.ts b/app/phone-calls/api/webhook/call.ts index ce43297..3b6865c 100644 --- a/app/phone-calls/api/webhook/call.ts +++ b/app/phone-calls/api/webhook/call.ts @@ -1,13 +1,11 @@ import type { BlitzApiRequest, BlitzApiResponse } from "blitz"; -import { getConfig } from "blitz"; import twilio from "twilio"; -import type { CallInstance } from "twilio/lib/rest/api/v2010/account/call"; -import db, { CallStatus, Direction } from "../../../../db"; +import db, { Direction } from "../../../../db"; import appLogger from "../../../../integrations/logger"; -import { voiceUrl } from "../../../../integrations/twilio"; +import { translateCallStatus, voiceUrl } from "../../../../integrations/twilio"; +import updateCallDurationQueue from "../queue/update-call-duration"; -const { serverRuntimeConfig } = getConfig(); const logger = appLogger.child({ route: "/api/webhook/call" }); type ApiError = { @@ -60,13 +58,21 @@ export default async function incomingCallHandler(req: BlitzApiRequest, res: Bli id: req.body.CallSid, from: phoneNumber.number, to: req.body.To, - status: translateStatus(req.body.CallStatus), + status: translateCallStatus(req.body.CallStatus), direction: Direction.Outbound, - duration: "", // TODO + duration: "0", organizationId: phoneNumber.organization.id, phoneNumberId: phoneNumber.id, }, }); + await updateCallDurationQueue.enqueue( + { + organizationId: phoneNumber.organization.id, + callId: req.body.CallSid, + }, + { delay: "30s" }, + ); + const twiml = new twilio.twiml.VoiceResponse(); const dial = twiml.dial({ answerOnBridge: true, @@ -129,24 +135,3 @@ const outgoingBody = { From: "client:95267d60-3d35-4c36-9905-8543ecb4f174__673b461a-11ba-43a4-89d7-9e29403053d4", To: "+33613370787", }; - -function translateStatus(status: CallInstance["status"]): CallStatus { - switch (status) { - case "busy": - return CallStatus.Busy; - case "canceled": - return CallStatus.Canceled; - case "completed": - return CallStatus.Completed; - case "failed": - return CallStatus.Failed; - case "in-progress": - return CallStatus.InProgress; - case "no-answer": - return CallStatus.NoAnswer; - case "queued": - return CallStatus.Queued; - case "ringing": - return CallStatus.Ringing; - } -} diff --git a/app/public-area/pages/open-metrics.tsx b/app/public-area/pages/open-metrics.tsx index f2ae7a2..26f55e5 100644 --- a/app/public-area/pages/open-metrics.tsx +++ b/app/public-area/pages/open-metrics.tsx @@ -24,7 +24,7 @@ const OpenMetrics: BlitzPage = () => { ); }; -function Card({ title, value }: any) { +function Card({ title, value }: { title: string; value: number | string }) { return (
{title}
diff --git a/integrations/twilio.ts b/integrations/twilio.ts index c1663a1..888ca80 100644 --- a/integrations/twilio.ts +++ b/integrations/twilio.ts @@ -1,7 +1,10 @@ import { getConfig, NotFoundError } from "blitz"; +import type { MessageInstance } from "twilio/lib/rest/api/v2010/account/message"; +import type { CallInstance } from "twilio/lib/rest/api/v2010/account/call"; import twilio from "twilio"; import type { Organization } from "db"; +import { CallStatus, Direction, MessageStatus } from "../db"; type MinimalOrganization = Pick; @@ -36,3 +39,77 @@ export function getTwiMLName() { return "Shellphone"; } } + +export function translateMessageStatus(status: MessageInstance["status"]): MessageStatus { + switch (status) { + case "accepted": + return MessageStatus.Accepted; + case "canceled": + return MessageStatus.Canceled; + case "delivered": + return MessageStatus.Delivered; + case "failed": + return MessageStatus.Failed; + case "partially_delivered": + return MessageStatus.PartiallyDelivered; + case "queued": + return MessageStatus.Queued; + case "read": + return MessageStatus.Read; + case "received": + return MessageStatus.Received; + case "receiving": + return MessageStatus.Receiving; + case "scheduled": + return MessageStatus.Scheduled; + case "sending": + return MessageStatus.Sending; + case "sent": + return MessageStatus.Sent; + case "undelivered": + return MessageStatus.Undelivered; + } +} + +export function translateMessageDirection(direction: MessageInstance["direction"]): Direction { + switch (direction) { + case "inbound": + return Direction.Inbound; + case "outbound-api": + case "outbound-call": + case "outbound-reply": + default: + return Direction.Outbound; + } +} + +export function translateCallStatus(status: CallInstance["status"]): CallStatus { + switch (status) { + case "busy": + return CallStatus.Busy; + case "canceled": + return CallStatus.Canceled; + case "completed": + return CallStatus.Completed; + case "failed": + return CallStatus.Failed; + case "in-progress": + return CallStatus.InProgress; + case "no-answer": + return CallStatus.NoAnswer; + case "queued": + return CallStatus.Queued; + case "ringing": + return CallStatus.Ringing; + } +} + +export function translateCallDirection(direction: CallInstance["direction"]): Direction { + switch (direction) { + case "inbound": + return Direction.Inbound; + case "outbound": + default: + return Direction.Outbound; + } +}