add recipient field to messages and phone calls

This commit is contained in:
m5r 2022-05-22 01:17:43 +02:00
parent 6684dcc0e5
commit 1f37eb45d5
6 changed files with 78 additions and 58 deletions

View File

@ -1,14 +1,14 @@
import type { LoaderFunction } from "@remix-run/node"; import type { LoaderFunction } from "@remix-run/node";
import { json } from "superjson-remix"; import { json } from "superjson-remix";
import { parsePhoneNumber } from "awesome-phonenumber"; import { parsePhoneNumber } from "awesome-phonenumber";
import { type Message, Prisma, Direction } from "@prisma/client"; import { type Message, Prisma } from "@prisma/client";
import db from "~/utils/db.server"; import db from "~/utils/db.server";
import { requireLoggedIn, type SessionData } from "~/utils/auth.server"; import { requireLoggedIn, type SessionData } from "~/utils/auth.server";
export type MessagesLoaderData = { export type MessagesLoaderData = {
user: { hasPhoneNumber: boolean }; user: { hasPhoneNumber: boolean };
conversations: Record<string, Conversation> | undefined; conversations: Conversations | undefined;
}; };
type Conversation = { type Conversation = {
@ -18,23 +18,23 @@ type Conversation = {
}; };
const loader: LoaderFunction = async ({ request }) => { const loader: LoaderFunction = async ({ request }) => {
const sessionData = await requireLoggedIn(request); const { phoneNumber } = await requireLoggedIn(request);
return json<MessagesLoaderData>({ return json<MessagesLoaderData>({
user: { hasPhoneNumber: Boolean(sessionData.phoneNumber) }, user: { hasPhoneNumber: Boolean(phoneNumber) },
conversations: await getConversations(sessionData.phoneNumber), conversations: await getConversations(phoneNumber),
}); });
}; };
export default loader; export default loader;
type Conversations = Record<string, Conversation>;
async function getConversations(sessionPhoneNumber: SessionData["phoneNumber"]) { async function getConversations(sessionPhoneNumber: SessionData["phoneNumber"]) {
if (!sessionPhoneNumber) { if (!sessionPhoneNumber) {
return; return;
} }
const phoneNumber = await db.phoneNumber.findUnique({ const phoneNumber = await db.phoneNumber.findUnique({ where: { id: sessionPhoneNumber.id } });
where: { id: sessionPhoneNumber.id },
});
if (!phoneNumber || phoneNumber.isFetchingMessages) { if (!phoneNumber || phoneNumber.isFetchingMessages) {
return; return;
} }
@ -42,34 +42,22 @@ async function getConversations(sessionPhoneNumber: SessionData["phoneNumber"])
const messages = await db.message.findMany({ const messages = await db.message.findMany({
where: { phoneNumberId: phoneNumber.id }, where: { phoneNumberId: phoneNumber.id },
orderBy: { sentAt: Prisma.SortOrder.desc }, orderBy: { sentAt: Prisma.SortOrder.desc },
distinct: "recipient",
}); });
let conversations: Record<string, Conversation> = {}; return messages.reduce<Conversations>((conversations, message) => {
for (const message of messages) { const recipient = message.recipient;
let recipient: string;
if (message.direction === Direction.Outbound) {
recipient = message.to;
} else {
recipient = message.from;
}
const formattedPhoneNumber = parsePhoneNumber(recipient).getNumber("international"); const formattedPhoneNumber = parsePhoneNumber(recipient).getNumber("international");
if (!conversations[recipient]) {
conversations[recipient] = { conversations[recipient] = {
recipient, recipient,
formattedPhoneNumber, formattedPhoneNumber,
lastMessage: message, lastMessage: message,
}; };
}
if (message.sentAt > conversations[recipient].lastMessage.sentAt) {
conversations[recipient].lastMessage = message;
}
/*conversations[recipient]!.messages.push({ /*conversations[recipient]!.messages.push({
...message, ...message,
content: decrypt(message.content, organization.encryptionKey), content: decrypt(message.content, organization.encryptionKey),
});*/ });*/
}
return conversations; return conversations;
}, {});
} }

View File

@ -4,7 +4,8 @@ import { z } from "zod";
import db from "~/utils/db.server"; import db from "~/utils/db.server";
import { type FormError, validate } from "~/utils/validation.server"; import { type FormError, validate } from "~/utils/validation.server";
import { requireLoggedIn } from "~/utils/auth.server"; import { refreshSessionData, requireLoggedIn } from "~/utils/auth.server";
import { commitSession } from "~/utils/session.server";
import setTwilioWebhooksQueue from "~/queues/set-twilio-webhooks.server"; import setTwilioWebhooksQueue from "~/queues/set-twilio-webhooks.server";
type SetPhoneNumberFailureActionData = { errors: FormError<typeof bodySchema>; submitted?: never }; type SetPhoneNumberFailureActionData = { errors: FormError<typeof bodySchema>; submitted?: never };
@ -39,9 +40,16 @@ const action: ActionFunction = async ({ request }) => {
phoneNumberId: validation.data.phoneNumberSid, phoneNumberId: validation.data.phoneNumberSid,
organizationId: organization.id, organizationId: organization.id,
}); });
console.log("queued"); const { session } = await refreshSessionData(request);
return json<SetPhoneNumberActionData>({ submitted: true }); return json<SetPhoneNumberActionData>(
{ submitted: true },
{
headers: {
"Set-Cookie": await commitSession(session),
},
},
);
}; };
export default action; export default action;

View File

@ -1,5 +1,5 @@
import type { MessageInstance } from "twilio/lib/rest/api/v2010/account/message"; import type { MessageInstance } from "twilio/lib/rest/api/v2010/account/message";
import type { Message } from "@prisma/client"; import { type Message, Direction } from "@prisma/client";
import { Queue } from "~/utils/queue.server"; import { Queue } from "~/utils/queue.server";
import db from "~/utils/db.server"; import db from "~/utils/db.server";
@ -22,20 +22,25 @@ export default Queue<Payload>("insert messages", async ({ data }) => {
} }
const sms = messages const sms = messages
.map<Message>((message) => ({ .map<Message>((message) => {
const status = translateMessageStatus(message.status);
const direction = translateMessageDirection(message.direction);
return {
id: message.sid, id: message.sid,
phoneNumberId: phoneNumber.id, phoneNumberId: phoneNumber.id,
content: message.body, content: message.body,
recipient: direction === Direction.Outbound ? message.to : message.from,
from: message.from, from: message.from,
to: message.to, to: message.to,
status: translateMessageStatus(message.status), status,
direction: translateMessageDirection(message.direction), direction,
sentAt: new Date(message.dateCreated), sentAt: new Date(message.dateCreated),
})) };
})
.sort((a, b) => a.sentAt.getTime() - b.sentAt.getTime()); .sort((a, b) => a.sentAt.getTime() - b.sentAt.getTime());
const { count } = await db.message.createMany({ data: sms, skipDuplicates: true }); const { count } = await db.message.createMany({ data: sms, skipDuplicates: true });
logger.info(`inserted ${count} new messages for phoneNumberId=${phoneNumberId}`) logger.info(`inserted ${count} new messages for phoneNumberId=${phoneNumberId}`);
if (!phoneNumber.isFetchingMessages) { if (!phoneNumber.isFetchingMessages) {
return; return;

View File

@ -1,5 +1,5 @@
import type { CallInstance } from "twilio/lib/rest/api/v2010/account/call"; import type { CallInstance } from "twilio/lib/rest/api/v2010/account/call";
import type { PhoneCall } from "@prisma/client"; import { type PhoneCall, Direction } from "@prisma/client";
import { Queue } from "~/utils/queue.server"; import { Queue } from "~/utils/queue.server";
import db from "~/utils/db.server"; import db from "~/utils/db.server";
@ -22,16 +22,21 @@ export default Queue<Payload>("insert phone calls", async ({ data }) => {
} }
const phoneCalls = calls const phoneCalls = calls
.map<PhoneCall>((call) => ({ .map<PhoneCall>((call) => {
const direction = translateCallDirection(call.direction);
const status = translateCallStatus(call.status);
return {
phoneNumberId, phoneNumberId,
id: call.sid, id: call.sid,
recipient: direction === Direction.Outbound ? call.to : call.from,
from: call.from, from: call.from,
to: call.to, to: call.to,
direction: translateCallDirection(call.direction), direction,
status: translateCallStatus(call.status), status,
duration: call.duration, duration: call.duration,
createdAt: new Date(call.dateCreated), createdAt: new Date(call.dateCreated),
})) };
})
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); .sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
const ddd = await db.phoneCall.createMany({ data: phoneCalls, skipDuplicates: true }); const ddd = await db.phoneCall.createMany({ data: phoneCalls, skipDuplicates: true });

View File

@ -118,6 +118,7 @@ CREATE TABLE "Message" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"sentAt" TIMESTAMPTZ NOT NULL, "sentAt" TIMESTAMPTZ NOT NULL,
"content" TEXT NOT NULL, "content" TEXT NOT NULL,
"recipient" TEXT NOT NULL,
"from" TEXT NOT NULL, "from" TEXT NOT NULL,
"to" TEXT NOT NULL, "to" TEXT NOT NULL,
"direction" "Direction" NOT NULL, "direction" "Direction" NOT NULL,
@ -131,6 +132,7 @@ CREATE TABLE "Message" (
CREATE TABLE "PhoneCall" ( CREATE TABLE "PhoneCall" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"recipient" TEXT NOT NULL,
"from" TEXT NOT NULL, "from" TEXT NOT NULL,
"to" TEXT NOT NULL, "to" TEXT NOT NULL,
"status" "CallStatus" NOT NULL, "status" "CallStatus" NOT NULL,
@ -172,11 +174,17 @@ CREATE UNIQUE INDEX "Token_membershipId_key" ON "Token"("membershipId");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "Token_hashedToken_type_key" ON "Token"("hashedToken", "type"); CREATE UNIQUE INDEX "Token_hashedToken_type_key" ON "Token"("hashedToken", "type");
-- CreateIndex
CREATE INDEX "Message_phoneNumberId_recipient_idx" ON "Message"("phoneNumberId", "recipient");
-- CreateIndex
CREATE INDEX "PhoneCall_phoneNumberId_recipient_idx" ON "PhoneCall"("phoneNumberId", "recipient");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "PhoneNumber_organizationId_isCurrent_key" ON "PhoneNumber"("organizationId", "isCurrent") WHERE ("isCurrent" = true); CREATE UNIQUE INDEX "PhoneNumber_organizationId_isCurrent_key" ON "PhoneNumber"("organizationId", "isCurrent") WHERE ("isCurrent" = true);
-- AddForeignKey -- AddForeignKey
ALTER TABLE "TwilioAccount" ADD CONSTRAINT "TwilioAccount_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "TwilioAccount" ADD CONSTRAINT "TwilioAccount_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey

View File

@ -17,7 +17,7 @@ model TwilioAccount {
twimlAppSid String? twimlAppSid String?
organizationId String @unique organizationId String @unique
organization Organization @relation(fields: [organizationId], references: [id]) organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
} }
model Organization { model Organization {
@ -105,17 +105,21 @@ model Message {
id String @id id String @id
sentAt DateTime @db.Timestamptz(6) sentAt DateTime @db.Timestamptz(6)
content String content String
recipient String
from String from String
to String to String
direction Direction direction Direction
status MessageStatus status MessageStatus
phoneNumberId String phoneNumberId String
phoneNumber PhoneNumber @relation(fields: [phoneNumberId], references: [id], onDelete: Cascade) phoneNumber PhoneNumber @relation(fields: [phoneNumberId], references: [id], onDelete: Cascade)
@@index([phoneNumberId, recipient])
} }
model PhoneCall { model PhoneCall {
id String @id id String @id
createdAt DateTime @default(now()) @db.Timestamptz(6) createdAt DateTime @default(now()) @db.Timestamptz(6)
recipient String
from String from String
to String to String
status CallStatus status CallStatus
@ -123,6 +127,8 @@ model PhoneCall {
duration String duration String
phoneNumberId String phoneNumberId String
phoneNumber PhoneNumber @relation(fields: [phoneNumberId], references: [id], onDelete: Cascade) phoneNumber PhoneNumber @relation(fields: [phoneNumberId], references: [id], onDelete: Cascade)
@@index([phoneNumberId, recipient])
} }
model PhoneNumber { model PhoneNumber {