({ phoneNumbers });
};
+export const action = settingsPhoneAction;
+
function PhoneSettings() {
return (
diff --git a/app/routes/twilio.authorize.ts b/app/routes/twilio.authorize.ts
index 58e186d..272e35d 100644
--- a/app/routes/twilio.authorize.ts
+++ b/app/routes/twilio.authorize.ts
@@ -1,26 +1,65 @@
import { type LoaderFunction, redirect } from "@remix-run/node";
-import { refreshSessionData, requireLoggedIn } from "~/utils/auth.server";
-import db from "~/utils/db.server";
-import { commitSession } from "~/utils/session.server";
import twilio from "twilio";
+
+import { refreshSessionData, requireLoggedIn } from "~/utils/auth.server";
+import { commitSession } from "~/utils/session.server";
+import db from "~/utils/db.server";
import serverConfig from "~/config/config.server";
+import getTwilioClient from "~/utils/twilio.server";
+import fetchPhoneCallsQueue from "~/queues/fetch-phone-calls.server";
+import fetchMessagesQueue from "~/queues/fetch-messages.server";
export const loader: LoaderFunction = async ({ request }) => {
const user = await requireLoggedIn(request);
+ const organization = user.organizations[0];
const url = new URL(request.url);
const twilioSubAccountSid = url.searchParams.get("AccountSid");
if (!twilioSubAccountSid) {
throw new Error("unreachable");
}
- const twilioClient = twilio(twilioSubAccountSid, serverConfig.twilio.authToken);
+ let twilioClient = twilio(twilioSubAccountSid, serverConfig.twilio.authToken);
const twilioSubAccount = await twilioClient.api.accounts(twilioSubAccountSid).fetch();
const twilioAccountSid = twilioSubAccount.ownerAccountSid;
await db.organization.update({
- where: { id: user.organizations[0].id },
+ where: { id: organization.id },
data: { twilioSubAccountSid, twilioAccountSid },
});
+ twilioClient = getTwilioClient({ twilioAccountSid, twilioSubAccountSid });
+ const phoneNumbers = await twilioClient.incomingPhoneNumbers.list();
+ await Promise.all(
+ phoneNumbers.map(async (phoneNumber) => {
+ const phoneNumberId = phoneNumber.sid;
+ try {
+ await db.phoneNumber.create({
+ data: {
+ id: phoneNumberId,
+ organizationId: organization.id,
+ number: phoneNumber.phoneNumber,
+ isCurrent: false,
+ isFetchingCalls: true,
+ isFetchingMessages: true,
+ },
+ });
+
+ await Promise.all([
+ fetchPhoneCallsQueue.add(`fetch calls of id=${phoneNumberId}`, {
+ phoneNumberId,
+ }),
+ fetchMessagesQueue.add(`fetch messages of id=${phoneNumberId}`, {
+ phoneNumberId,
+ }),
+ ]);
+ } catch (error: any) {
+ if (error.code !== "P2002") {
+ // if it's not a duplicate, it's a real error we need to handle
+ throw error;
+ }
+ }
+ }),
+ );
+
const { session } = await refreshSessionData(request);
return redirect("/settings/phone", {
headers: {
diff --git a/app/utils/twilio.server.ts b/app/utils/twilio.server.ts
index 4a20b78..7167c3b 100644
--- a/app/utils/twilio.server.ts
+++ b/app/utils/twilio.server.ts
@@ -1,16 +1,92 @@
import twilio from "twilio";
+import type { MessageInstance } from "twilio/lib/rest/api/v2010/account/message";
+import type { CallInstance } from "twilio/lib/rest/api/v2010/account/call";
+import { type Organization, CallStatus, Direction, MessageStatus } from "@prisma/client";
-import type { Organization } from "@prisma/client";
import serverConfig from "~/config/config.server";
type MinimalOrganization = Pick;
-export default function getTwilioClient(organization: MinimalOrganization): twilio.Twilio {
- if (!organization || !organization.twilioSubAccountSid || !organization.twilioAccountSid) {
+export default function getTwilioClient({ twilioAccountSid, twilioSubAccountSid }: MinimalOrganization): twilio.Twilio {
+ if (!twilioSubAccountSid || !twilioAccountSid) {
throw new Error("unreachable");
}
- return twilio(organization.twilioSubAccountSid, serverConfig.twilio.authToken, {
- accountSid: organization.twilioAccountSid,
+ return twilio(twilioSubAccountSid, serverConfig.twilio.authToken, {
+ accountSid: twilioAccountSid,
});
}
+
+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;
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index a2fc1d3..3097d54 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -41,6 +41,7 @@
"remix-utils": "3.2.0",
"secure-password": "4.0.0",
"stripe": "9.1.0",
+ "superjson-remix": "0.1.2",
"tiny-invariant": "1.2.0",
"tslog": "3.3.3",
"twilio": "3.77.0",
@@ -6898,6 +6899,20 @@
"node": ">=6.6.0"
}
},
+ "node_modules/copy-anything": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.2.tgz",
+ "integrity": "sha512-CzATjGXzUQ0EvuvgOCI6A4BGOo2bcVx8B+eC2nF862iv9fopnPQwlrbACakNCHRIJbCSBj+J/9JeDf60k64MkA==",
+ "dependencies": {
+ "is-what": "^4.1.6"
+ },
+ "engines": {
+ "node": ">=12.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
"node_modules/copy-descriptor": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
@@ -11947,6 +11962,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-what": {
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.7.tgz",
+ "integrity": "sha512-DBVOQNiPKnGMxRMLIYSwERAS5MVY1B7xYiGnpgctsOFvVDz9f9PFXXxMcTOHuoqYp4NK9qFYQaIC1NRRxLMpBQ==",
+ "engines": {
+ "node": ">=12.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
"node_modules/is-whitespace": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/is-whitespace/-/is-whitespace-0.3.0.tgz",
@@ -19751,6 +19777,32 @@
"postcss": "^8.2.15"
}
},
+ "node_modules/superjson": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.9.1.tgz",
+ "integrity": "sha512-oT3HA2nPKlU1+5taFgz/HDy+GEaY+CWEbLzaRJVD4gZ7zMVVC4GDNFdgvAZt6/VuIk6D2R7RtPAiCHwmdzlMmg==",
+ "dependencies": {
+ "copy-anything": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/superjson-remix": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/superjson-remix/-/superjson-remix-0.1.2.tgz",
+ "integrity": "sha512-NkJd+V0zOMCUbwnaKsxNs1FXMWTAHppqaCTd9IKXDOoPtRkjxU8/7duQeJ9yC8L3J9eJ47CShkIS+NEhzlY6IQ==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "superjson": "^1.8.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "remix": ">=1.2.3"
+ }
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -26805,6 +26857,14 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.0.tgz",
"integrity": "sha512-R0BOPfLGTitaKhgKROKZQN6iyq2iDQcH1DOF8nJoaWapguX5bC2w+Q/I9NmmM5lfcvEarnLZr+cCvmEYYSXvYA=="
},
+ "copy-anything": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.2.tgz",
+ "integrity": "sha512-CzATjGXzUQ0EvuvgOCI6A4BGOo2bcVx8B+eC2nF862iv9fopnPQwlrbACakNCHRIJbCSBj+J/9JeDf60k64MkA==",
+ "requires": {
+ "is-what": "^4.1.6"
+ }
+ },
"copy-descriptor": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
@@ -30493,6 +30553,11 @@
"call-bind": "^1.0.2"
}
},
+ "is-what": {
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.7.tgz",
+ "integrity": "sha512-DBVOQNiPKnGMxRMLIYSwERAS5MVY1B7xYiGnpgctsOFvVDz9f9PFXXxMcTOHuoqYp4NK9qFYQaIC1NRRxLMpBQ=="
+ },
"is-whitespace": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/is-whitespace/-/is-whitespace-0.3.0.tgz",
@@ -36394,6 +36459,22 @@
"postcss-selector-parser": "^6.0.4"
}
},
+ "superjson": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.9.1.tgz",
+ "integrity": "sha512-oT3HA2nPKlU1+5taFgz/HDy+GEaY+CWEbLzaRJVD4gZ7zMVVC4GDNFdgvAZt6/VuIk6D2R7RtPAiCHwmdzlMmg==",
+ "requires": {
+ "copy-anything": "^3.0.2"
+ }
+ },
+ "superjson-remix": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/superjson-remix/-/superjson-remix-0.1.2.tgz",
+ "integrity": "sha512-NkJd+V0zOMCUbwnaKsxNs1FXMWTAHppqaCTd9IKXDOoPtRkjxU8/7duQeJ9yC8L3J9eJ47CShkIS+NEhzlY6IQ==",
+ "requires": {
+ "superjson": "^1.8.1"
+ }
+ },
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
diff --git a/package.json b/package.json
index 32d14d3..be5c18c 100644
--- a/package.json
+++ b/package.json
@@ -82,6 +82,7 @@
"remix-utils": "3.2.0",
"secure-password": "4.0.0",
"stripe": "9.1.0",
+ "superjson-remix": "0.1.2",
"tiny-invariant": "1.2.0",
"tslog": "3.3.3",
"twilio": "3.77.0",