From e4672597dd04c82084d0df835b1e0acb616211f0 Mon Sep 17 00:00:00 2001 From: m5r Date: Sun, 8 Aug 2021 14:37:53 +0800 Subject: [PATCH] get access token and use it to make a call --- app/phone-calls/api/webhook/incoming-call.ts | 44 +++- app/phone-calls/mutations/get-token.ts | 40 ++++ app/{keypad => phone-calls}/pages/keypad.tsx | 10 +- .../pages/outgoing-call/[recipient].tsx | 215 ++++++++++++++++++ .../queries/get-current-phone-number.ts | 1 + 5 files changed, 304 insertions(+), 6 deletions(-) create mode 100644 app/phone-calls/mutations/get-token.ts rename app/{keypad => phone-calls}/pages/keypad.tsx (91%) create mode 100644 app/phone-calls/pages/outgoing-call/[recipient].tsx diff --git a/app/phone-calls/api/webhook/incoming-call.ts b/app/phone-calls/api/webhook/incoming-call.ts index e31b6b5..d7249e2 100644 --- a/app/phone-calls/api/webhook/incoming-call.ts +++ b/app/phone-calls/api/webhook/incoming-call.ts @@ -1,3 +1,43 @@ -import type { NextApiRequest, NextApiResponse } from "next"; +import type { BlitzApiRequest, BlitzApiResponse } from "blitz"; +import Twilio from "twilio"; -export default async function incomingCallHandler(req: NextApiRequest, res: NextApiResponse) {} +import db from "../../../../db"; + +export default async function incomingCallHandler(req: BlitzApiRequest, res: BlitzApiResponse) { + console.log("req.body", req.body); + + const isOutgoingCall = true; + if (isOutgoingCall) { + const recipient = req.body.To; + const organizationId = req.body.From.slice("client:".length).split("__")[0]; + const phoneNumber = await db.phoneNumber.findFirst({ + where: { organizationId }, + select: { number: true }, + }); + const twiml = new Twilio.twiml.VoiceResponse(); + const dial = twiml.dial({ + answerOnBridge: true, + callerId: phoneNumber!.number, + }); + dial.number(recipient); + console.log("twiml", twiml.toString()); + + res.setHeader("content-type", "text/xml"); + return res.status(200).send(twiml.toString()); + } + + res.status(500).end(); +} + +const outgoingBody = { + AccountSid: "ACa886d066be0832990d1cf43fb1d53362", + ApiVersion: "2010-04-01", + ApplicationSid: "AP6334c6dd54f5808717b37822de4e4e14", + CallSid: "CA3b639875693fd8f563e07937780c9f5f", + CallStatus: "ringing", + Called: "", + Caller: "client:95267d60-3d35-4c36-9905-8543ecb4f174__673b461a-11ba-43a4-89d7-9e29403053d4", + Direction: "inbound", + From: "client:95267d60-3d35-4c36-9905-8543ecb4f174__673b461a-11ba-43a4-89d7-9e29403053d4", + To: "+33613370787", +}; diff --git a/app/phone-calls/mutations/get-token.ts b/app/phone-calls/mutations/get-token.ts new file mode 100644 index 0000000..926b9c8 --- /dev/null +++ b/app/phone-calls/mutations/get-token.ts @@ -0,0 +1,40 @@ +import { resolver, NotFoundError } from "blitz"; +import Twilio from "twilio"; + +import db from "db"; +import getCurrentPhoneNumber from "../../phone-numbers/queries/get-current-phone-number"; + +export default resolver.pipe(resolver.authorize(), async (_ = null, context) => { + const phoneNumber = await getCurrentPhoneNumber({}, context); + if (!phoneNumber) { + throw new NotFoundError(); + } + + const organization = await db.organization.findFirst({ + where: { id: phoneNumber.organizationId }, + }); + if ( + !organization || + !organization.twilioAccountSid || + !organization.twilioAuthToken || + !organization.twilioApiKey || + !organization.twilioApiSecret || + !organization.twimlAppSid + ) { + throw new NotFoundError(); + } + + const accessToken = new Twilio.jwt.AccessToken( + organization.twilioAccountSid, + organization.twilioApiKey, + organization.twilioApiSecret, + { identity: `${context.session.orgId}__${context.session.userId}` }, + ); + const grant = new Twilio.jwt.AccessToken.VoiceGrant({ + outgoingApplicationSid: organization.twimlAppSid, + incomingAllow: true, + }); + accessToken.addGrant(grant); + + return accessToken.toJwt(); +}); diff --git a/app/keypad/pages/keypad.tsx b/app/phone-calls/pages/keypad.tsx similarity index 91% rename from app/keypad/pages/keypad.tsx rename to app/phone-calls/pages/keypad.tsx index 4ac72c0..18d7e20 100644 --- a/app/keypad/pages/keypad.tsx +++ b/app/phone-calls/pages/keypad.tsx @@ -1,5 +1,5 @@ import type { BlitzPage } from "blitz"; -import { Routes } from "blitz"; +import { Link, Routes } from "blitz"; import type { FunctionComponent } from "react"; import { useRef } from "react"; import { atom, useAtom } from "jotai"; @@ -59,9 +59,11 @@ const Keypad: BlitzPage = () => { -
- -
+ + + + +
diff --git a/app/phone-calls/pages/outgoing-call/[recipient].tsx b/app/phone-calls/pages/outgoing-call/[recipient].tsx new file mode 100644 index 0000000..b0ba02c --- /dev/null +++ b/app/phone-calls/pages/outgoing-call/[recipient].tsx @@ -0,0 +1,215 @@ +import type { BlitzPage } from "blitz"; +import { Routes, useMutation, useRouter } from "blitz"; +import type { FunctionComponent } from "react"; +import { useEffect, useRef, useState } from "react"; +import { atom, useAtom } from "jotai"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faPhoneAlt as faPhone } from "@fortawesome/pro-solid-svg-icons"; +import { usePress } from "@react-aria/interactions"; +import type { TwilioError } from "@twilio/voice-sdk"; +import { Device, Call } from "@twilio/voice-sdk"; + +import getToken from "../../mutations/get-token"; +import useRequireOnboarding from "../../../core/hooks/use-require-onboarding"; + +const OutgoingCall: BlitzPage = () => { + const router = useRouter(); + const recipient = decodeURIComponent(router.params.recipient); + const [device, setDevice] = useState(null); + const [getTokenMutation] = useMutation(getToken); + async function makeCall() { + const token = await getTokenMutation(); + console.log("token", token); + const device = new Device(token, { codecPreferences: [Call.Codec.Opus, Call.Codec.PCMU] }); + setDevice(device); + + const params = { To: recipient }; + const outgoingConnection = await device.connect({ params }); + // @ts-ignore + window.ddd = outgoingConnection; + + /*$("[id^=dial-]").click(function (event) { + console.log("send digit", event.target.innerText); + outgoingConnection.sendDigits(event.target.innerText); + })*/ + + outgoingConnection.on("ringing", () => { + console.log("Ringing..."); + }); + } + + useEffect(() => { + if (!device) { + return; + } + + device.on("ready", onDeviceReady); + device.on("error", onDeviceError); + device.on("register", onDeviceRegistered); + device.on("unregister", onDeviceUnregistered); + device.on("incoming", onDeviceIncoming); + // device.audio?.on('deviceChange', updateAllDevices.bind(device)); + + return () => { + device.off("ready", onDeviceReady); + device.off("error", onDeviceError); + device.off("register", onDeviceRegistered); + device.off("unregister", onDeviceUnregistered); + device.off("incoming", onDeviceIncoming); + }; + }, [device]); + + useRequireOnboarding(); + const phoneNumber = useAtom(phoneNumberAtom)[0]; + + return ( +
+
+ {phoneNumber} +
+ +
+ + + + ABC + + + DEF + + + + + GHI + + + JKL + + + MNO + + + + + PQRS + + + TUV + + + WXYZ + + + + + + + + +
+ +
+
+
+
+ ); +}; + +const ZeroDigit: FunctionComponent = () => { + const timeoutRef = useRef | null>(null); + const pressDigit = useAtom(pressDigitAtom)[1]; + const longPressDigit = useAtom(longPressDigitAtom)[1]; + const { pressProps } = usePress({ + onPressStart() { + pressDigit("0"); + timeoutRef.current = setTimeout(() => { + longPressDigit("+"); + }, 750); + }, + onPressEnd() { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }, + }); + + return ( +
+ 0 + +
+ ); +}; + +const Row: FunctionComponent = ({ children }) => ( +
{children}
+); + +const Digit: FunctionComponent<{ digit: string }> = ({ children, digit }) => { + const pressDigit = useAtom(pressDigitAtom)[1]; + const { pressProps } = usePress({ + onPress() { + pressDigit(digit); + }, + }); + + return ( +
+ {digit} + {children} +
+ ); +}; + +const DigitLetters: FunctionComponent = ({ children }) =>
{children}
; + +const phoneNumberAtom = atom(""); +const pressDigitAtom = atom(null, (get, set, digit: string) => { + if (get(phoneNumberAtom).length > 17) { + return; + } + + set(phoneNumberAtom, (prevState) => prevState + digit); +}); +const longPressDigitAtom = atom(null, (get, set, replaceWith: string) => { + if (get(phoneNumberAtom).length > 17) { + return; + } + + set(phoneNumberAtom, (prevState) => prevState.slice(0, -1) + replaceWith); +}); + +OutgoingCall.authenticate = { redirectTo: Routes.SignIn() }; + +function onDeviceReady(device: Device) { + console.log("device", device); +} + +function onDeviceError(error: TwilioError.TwilioError, call?: Call) { + console.log("error", error); +} + +function onDeviceRegistered(device: Device) { + console.log("ready to make calls"); + console.log("device", device); +} + +function onDeviceUnregistered() {} + +function onDeviceIncoming(call: Call) { + console.log("call", call); + console.log("Incoming connection from " + call.parameters.From); + let archEnemyPhoneNumber = "+12093373517"; + + if (call.parameters.From === archEnemyPhoneNumber) { + call.reject(); + console.log("It's your nemesis. Rejected call."); + } else { + // accept the incoming connection and start two-way audio + call.accept(); + } +} + +export default OutgoingCall; diff --git a/app/phone-numbers/queries/get-current-phone-number.ts b/app/phone-numbers/queries/get-current-phone-number.ts index 601e9ab..1762491 100644 --- a/app/phone-numbers/queries/get-current-phone-number.ts +++ b/app/phone-numbers/queries/get-current-phone-number.ts @@ -14,6 +14,7 @@ export default resolver.pipe( where: { organizationId }, select: { id: true, + organizationId: true, number: true, }, });