list phone calls
This commit is contained in:
parent
f55f1c5359
commit
a262f61823
@ -1,16 +0,0 @@
|
|||||||
export enum SmsType {
|
|
||||||
SENT = "sent",
|
|
||||||
RECEIVED = "received",
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Sms = {
|
|
||||||
id: string;
|
|
||||||
customerId: string;
|
|
||||||
content: string;
|
|
||||||
from: string;
|
|
||||||
to: string;
|
|
||||||
type: SmsType;
|
|
||||||
twilioSid?: string;
|
|
||||||
// status: sent/delivered/received
|
|
||||||
sentAt: string; // timestampz
|
|
||||||
};
|
|
@ -1,31 +1,44 @@
|
|||||||
|
import { MessageStatus } from "twilio/lib/rest/api/v2010/account/message";
|
||||||
|
|
||||||
import appLogger from "../../lib/logger";
|
import appLogger from "../../lib/logger";
|
||||||
import supabase from "../supabase/server";
|
import supabase from "../supabase/server";
|
||||||
import type { Sms } from "./_types";
|
|
||||||
import { findCustomer } from "./customer";
|
import { findCustomer } from "./customer";
|
||||||
import { decrypt } from "./_encryption";
|
import { decrypt } from "./_encryption";
|
||||||
|
|
||||||
const logger = appLogger.child({ module: "sms" });
|
const logger = appLogger.child({ module: "message" });
|
||||||
|
|
||||||
export async function insertSms(messages: Omit<Sms, "id" | "twilioSid">): Promise<Sms> {
|
export type Message = {
|
||||||
|
id: string;
|
||||||
|
customerId: string;
|
||||||
|
content: string;
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
direction: "inbound" | "outbound";
|
||||||
|
status: MessageStatus;
|
||||||
|
twilioSid?: string;
|
||||||
|
sentAt: string; // timestampz
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function insertMessage(message: Omit<Message, "id" | "twilioSid">): Promise<Message> {
|
||||||
const { error, data } = await supabase
|
const { error, data } = await supabase
|
||||||
.from<Sms>("sms")
|
.from<Message>("message")
|
||||||
.insert(messages);
|
.insert(message);
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
return data![0];
|
return data![0];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function insertManySms(messages: Omit<Sms, "id">[]) {
|
export async function insertManyMessage(messages: Omit<Message, "id">[]) {
|
||||||
await supabase
|
await supabase
|
||||||
.from<Sms>("sms")
|
.from<Message>("message")
|
||||||
.insert(messages)
|
.insert(messages)
|
||||||
.throwOnError();
|
.throwOnError();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function findCustomerMessages(customerId: Sms["customerId"]): Promise<Sms[]> {
|
export async function findCustomerMessages(customerId: Message["customerId"]): Promise<Message[]> {
|
||||||
const { error, data } = await supabase
|
const { error, data } = await supabase
|
||||||
.from<Sms>("sms")
|
.from<Message>("message")
|
||||||
.select("*")
|
.select("*")
|
||||||
.eq("customerId", customerId);
|
.eq("customerId", customerId);
|
||||||
|
|
||||||
@ -34,9 +47,9 @@ export async function findCustomerMessages(customerId: Sms["customerId"]): Promi
|
|||||||
return data!;
|
return data!;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function findCustomerMessageBySid({ customerId, twilioSid }: Pick<Sms, "customerId" | "twilioSid">): Promise<Sms> {
|
export async function findCustomerMessageBySid({ customerId, twilioSid }: Pick<Message, "customerId" | "twilioSid">): Promise<Message> {
|
||||||
const { error, data } = await supabase
|
const { error, data } = await supabase
|
||||||
.from<Sms>("sms")
|
.from<Message>("message")
|
||||||
.select("*")
|
.select("*")
|
||||||
.eq("customerId", customerId)
|
.eq("customerId", customerId)
|
||||||
.eq("twilioSid", twilioSid)
|
.eq("twilioSid", twilioSid)
|
||||||
@ -47,17 +60,17 @@ export async function findCustomerMessageBySid({ customerId, twilioSid }: Pick<S
|
|||||||
return data!;
|
return data!;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setTwilioSid({ id, twilioSid }: Pick<Sms, "id" | "twilioSid">) {
|
export async function setTwilioSid({ id, twilioSid }: Pick<Message, "id" | "twilioSid">) {
|
||||||
await supabase.from<Sms>("sms")
|
await supabase.from<Message>("message")
|
||||||
.update({ twilioSid })
|
.update({ twilioSid })
|
||||||
.eq("id", id)
|
.eq("id", id)
|
||||||
.throwOnError();
|
.throwOnError();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function findConversation(customerId: Sms["customerId"], recipient: Sms["to"]): Promise<Sms[]> {
|
export async function findConversation(customerId: Message["customerId"], recipient: Message["to"]): Promise<Message[]> {
|
||||||
const customer = await findCustomer(customerId);
|
const customer = await findCustomer(customerId);
|
||||||
const { error, data } = await supabase
|
const { error, data } = await supabase
|
||||||
.from<Sms>("sms")
|
.from<Message>("message")
|
||||||
.select("*")
|
.select("*")
|
||||||
.eq("customerId", customerId)
|
.eq("customerId", customerId)
|
||||||
.or(`to.eq.${recipient},from.eq.${recipient}`);
|
.or(`to.eq.${recipient},from.eq.${recipient}`);
|
53
src/database/phone-call.ts
Normal file
53
src/database/phone-call.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import type { CallStatus } from "twilio/lib/rest/api/v2010/account/call";
|
||||||
|
|
||||||
|
import appLogger from "../../lib/logger";
|
||||||
|
import supabase from "../supabase/server";
|
||||||
|
|
||||||
|
const logger = appLogger.child({ module: "phone-call" });
|
||||||
|
|
||||||
|
export type PhoneCall = {
|
||||||
|
id: string;
|
||||||
|
customerId: string;
|
||||||
|
twilioSid: string;
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
status: CallStatus;
|
||||||
|
direction: "inbound" | "outbound";
|
||||||
|
duration: string;
|
||||||
|
createdAt: string; // timestampz
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function insertPhoneCall(phoneCall: Omit<PhoneCall, "id" | "twilioSid">): Promise<PhoneCall> {
|
||||||
|
const { error, data } = await supabase
|
||||||
|
.from<PhoneCall>("phone-call")
|
||||||
|
.insert(phoneCall);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return data![0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function insertManyPhoneCalls(phoneCalls: Omit<PhoneCall, "id">[]) {
|
||||||
|
await supabase
|
||||||
|
.from<PhoneCall>("phone-call")
|
||||||
|
.insert(phoneCalls)
|
||||||
|
.throwOnError();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findCustomerPhoneCalls(customerId: PhoneCall["customerId"]): Promise<PhoneCall[]> {
|
||||||
|
const { error, data } = await supabase
|
||||||
|
.from<PhoneCall>("phone-call")
|
||||||
|
.select("*")
|
||||||
|
.eq("customerId", customerId);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setTwilioSid({ id, twilioSid }: Pick<PhoneCall, "id" | "twilioSid">) {
|
||||||
|
await supabase.from<PhoneCall>("phone-call")
|
||||||
|
.update({ twilioSid })
|
||||||
|
.eq("id", id)
|
||||||
|
.throwOnError();
|
||||||
|
}
|
@ -1,11 +1,10 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import type { Sms } from "../database/_types";
|
import type { Message } from "../database/message";
|
||||||
import { SmsType } from "../database/_types";
|
|
||||||
import useUser from "./use-user";
|
import useUser from "./use-user";
|
||||||
|
|
||||||
type UseConversationParams = {
|
type UseConversationParams = {
|
||||||
initialData?: Sms[];
|
initialData?: Message[];
|
||||||
recipient: string;
|
recipient: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -16,11 +15,11 @@ export default function useConversation({
|
|||||||
const user = useUser();
|
const user = useUser();
|
||||||
const getConversationUrl = `/api/conversation/${encodeURIComponent(recipient)}`;
|
const getConversationUrl = `/api/conversation/${encodeURIComponent(recipient)}`;
|
||||||
const fetcher = async () => {
|
const fetcher = async () => {
|
||||||
const { data } = await axios.get<Sms[]>(getConversationUrl);
|
const { data } = await axios.get<Message[]>(getConversationUrl);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const getConversationQuery = useQuery<Sms[]>(
|
const getConversationQuery = useQuery<Message[]>(
|
||||||
getConversationUrl,
|
getConversationUrl,
|
||||||
fetcher,
|
fetcher,
|
||||||
{
|
{
|
||||||
@ -31,21 +30,22 @@ export default function useConversation({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const sendMessage = useMutation(
|
const sendMessage = useMutation(
|
||||||
(sms: Pick<Sms, "to" | "content">) => axios.post(`/api/conversation/${sms.to}/send-message`, sms, { withCredentials: true }),
|
(sms: Pick<Message, "to" | "content">) => axios.post(`/api/conversation/${sms.to}/send-message`, sms, { withCredentials: true }),
|
||||||
{
|
{
|
||||||
onMutate: async (sms: Pick<Sms, "to" | "content">) => {
|
onMutate: async (sms: Pick<Message, "to" | "content">) => {
|
||||||
await queryClient.cancelQueries(getConversationUrl);
|
await queryClient.cancelQueries(getConversationUrl);
|
||||||
const previousMessages = queryClient.getQueryData<Sms[]>(getConversationUrl);
|
const previousMessages = queryClient.getQueryData<Message[]>(getConversationUrl);
|
||||||
|
|
||||||
if (previousMessages) {
|
if (previousMessages) {
|
||||||
queryClient.setQueryData<Sms[]>(getConversationUrl, [
|
queryClient.setQueryData<Message[]>(getConversationUrl, [
|
||||||
...previousMessages,
|
...previousMessages,
|
||||||
{
|
{
|
||||||
id: "", // TODO: somehow generate an id
|
id: "", // TODO: somehow generate an id
|
||||||
from: "", // TODO: get user's phone number
|
from: "", // TODO: get user's phone number
|
||||||
customerId: user.userProfile!.id,
|
customerId: user.userProfile!.id,
|
||||||
sentAt: new Date().toISOString(),
|
sentAt: new Date().toISOString(),
|
||||||
type: SmsType.SENT,
|
direction: "outbound",
|
||||||
|
status: "queued",
|
||||||
content: sms.content,
|
content: sms.content,
|
||||||
to: sms.to,
|
to: sms.to,
|
||||||
},
|
},
|
||||||
@ -56,7 +56,7 @@ export default function useConversation({
|
|||||||
},
|
},
|
||||||
onError: (error, variables, context) => {
|
onError: (error, variables, context) => {
|
||||||
if (context?.previousMessages) {
|
if (context?.previousMessages) {
|
||||||
queryClient.setQueryData<Sms[]>(getConversationUrl, context.previousMessages);
|
queryClient.setQueryData<Message[]>(getConversationUrl, context.previousMessages);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSettled: () => queryClient.invalidateQueries(getConversationUrl),
|
onSettled: () => queryClient.invalidateQueries(getConversationUrl),
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Joi from "joi";
|
import Joi from "joi";
|
||||||
|
|
||||||
import { withApiAuthRequired } from "../../../../../lib/session-helpers";
|
import { withApiAuthRequired } from "../../../../../lib/session-helpers";
|
||||||
import { findConversation } from "../../../../database/sms";
|
import { findConversation } from "../../../../database/message";
|
||||||
import type { ApiError } from "../../_types";
|
import type { ApiError } from "../../_types";
|
||||||
import appLogger from "../../../../../lib/logger";
|
import appLogger from "../../../../../lib/logger";
|
||||||
|
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import Joi from "joi";
|
import Joi from "joi";
|
||||||
|
|
||||||
import { SmsType } from "../../../../database/_types";
|
|
||||||
import { withApiAuthRequired } from "../../../../../lib/session-helpers";
|
import { withApiAuthRequired } from "../../../../../lib/session-helpers";
|
||||||
import { findConversation, insertSms } from "../../../../database/sms";
|
import { insertMessage } from "../../../../database/message";
|
||||||
import type { ApiError } from "../../_types";
|
import type { ApiError } from "../../_types";
|
||||||
import appLogger from "../../../../../lib/logger";
|
import appLogger from "../../../../../lib/logger";
|
||||||
import { findCustomerPhoneNumber } from "../../../../database/phone-number";
|
import { findCustomerPhoneNumber } from "../../../../database/phone-number";
|
||||||
import { encrypt } from "../../../../database/_encryption";
|
import { encrypt } from "../../../../database/_encryption";
|
||||||
import { findCustomer } from "../../../../database/customer";
|
import { findCustomer } from "../../../../database/customer";
|
||||||
import twilio from "twilio";
|
|
||||||
import sendMessageQueue from "../../queue/send-message";
|
import sendMessageQueue from "../../queue/send-message";
|
||||||
|
|
||||||
const logger = appLogger.child({ route: "/api/conversation" });
|
const logger = appLogger.child({ route: "/api/conversation" });
|
||||||
@ -60,11 +58,12 @@ export default withApiAuthRequired(async function sendMessageHandler(
|
|||||||
const { phoneNumber } = await findCustomerPhoneNumber(customerId);
|
const { phoneNumber } = await findCustomerPhoneNumber(customerId);
|
||||||
const body: Body = validationResult.value;
|
const body: Body = validationResult.value;
|
||||||
|
|
||||||
const sms = await insertSms({
|
const sms = await insertMessage({
|
||||||
from: phoneNumber,
|
from: phoneNumber,
|
||||||
customerId: customerId,
|
customerId: customerId,
|
||||||
sentAt: new Date().toISOString(),
|
sentAt: new Date().toISOString(),
|
||||||
type: SmsType.SENT,
|
direction: "outbound",
|
||||||
|
status: "queued",
|
||||||
content: encrypt(body.content, customer.encryptionKey),
|
content: encrypt(body.content, customer.encryptionKey),
|
||||||
to: body.to,
|
to: body.to,
|
||||||
});
|
});
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { insertSms } from "../../database/sms";
|
import { insertMessage } from "../../database/message";
|
||||||
import { SmsType } from "../../database/_types";
|
|
||||||
import { encrypt } from "../../database/_encryption";
|
import { encrypt } from "../../database/_encryption";
|
||||||
import twilio from "twilio";
|
import twilio from "twilio";
|
||||||
|
import fetchCallsQueue from "./queue/fetch-calls";
|
||||||
|
|
||||||
export default async function ddd(req: NextApiRequest, res: NextApiResponse) {
|
export default async function ddd(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const accountSid = "ACa886d066be0832990d1cf43fb1d53362";
|
const accountSid = "ACa886d066be0832990d1cf43fb1d53362";
|
||||||
const authToken = "8696a59a64b94bb4eba3548ed815953b";
|
const authToken = "8696a59a64b94bb4eba3548ed815953b";
|
||||||
// const ddd = await twilio(accountSid, authToken).incomingPhoneNumbers.list();
|
// const ddd = await twilio(accountSid, authToken).incomingPhoneNumbers.list();
|
||||||
const phoneNumber = "+33757592025";
|
const phoneNumber = "+33757592025";
|
||||||
const ddd = await twilio(accountSid, authToken)
|
/*const ddd = await twilio(accountSid, authToken)
|
||||||
.messages
|
.messages
|
||||||
.list({
|
.list({
|
||||||
to: phoneNumber,
|
to: phoneNumber,
|
||||||
});
|
});*/
|
||||||
|
|
||||||
/*const ddd = await insertSms({
|
/*const ddd = await insertSms({
|
||||||
to: "+213",
|
to: "+213",
|
||||||
@ -43,6 +43,9 @@ export default async function ddd(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
voiceApplicationSid: appSid,
|
voiceApplicationSid: appSid,
|
||||||
});*/
|
});*/
|
||||||
|
|
||||||
|
const customerId = "bcb723bc-9706-4811-a964-cc03018bd2ac";
|
||||||
|
const ddd = fetchCallsQueue.enqueue({ customerId }, { id: `fetch-messages-${customerId}` })
|
||||||
|
|
||||||
console.log("ddd", ddd);
|
console.log("ddd", ddd);
|
||||||
|
|
||||||
return res.status(200).send(ddd);
|
return res.status(200).send(ddd);
|
||||||
|
40
src/pages/api/queue/fetch-calls.ts
Normal file
40
src/pages/api/queue/fetch-calls.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Queue } from "quirrel/next";
|
||||||
|
import twilio from "twilio";
|
||||||
|
|
||||||
|
import { findCustomerPhoneNumber } from "../../../database/phone-number";
|
||||||
|
import { findCustomer } from "../../../database/customer";
|
||||||
|
import insertCallsQueue from "./insert-calls";
|
||||||
|
|
||||||
|
type Payload = {
|
||||||
|
customerId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchCallsQueue = Queue<Payload>(
|
||||||
|
"api/queue/fetch-calls",
|
||||||
|
async ({ customerId }) => {
|
||||||
|
const customer = await findCustomer(customerId);
|
||||||
|
const phoneNumber = await findCustomerPhoneNumber(customerId);
|
||||||
|
|
||||||
|
const [callsSent, callsReceived] = await Promise.all([
|
||||||
|
twilio(customer.accountSid, customer.authToken)
|
||||||
|
.calls
|
||||||
|
.list({ from: phoneNumber.phoneNumber }),
|
||||||
|
twilio(customer.accountSid, customer.authToken)
|
||||||
|
.calls
|
||||||
|
.list({ to: phoneNumber.phoneNumber })
|
||||||
|
]);
|
||||||
|
const calls = [
|
||||||
|
...callsSent,
|
||||||
|
...callsReceived,
|
||||||
|
].sort((a, b) => a.dateCreated.getTime() - b.dateCreated.getTime());
|
||||||
|
|
||||||
|
await insertCallsQueue.enqueue({
|
||||||
|
customerId,
|
||||||
|
calls,
|
||||||
|
}, {
|
||||||
|
id: `insert-calls-${customerId}`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default fetchCallsQueue;
|
@ -15,12 +15,14 @@ const fetchMessagesQueue = Queue<Payload>(
|
|||||||
const customer = await findCustomer(customerId);
|
const customer = await findCustomer(customerId);
|
||||||
const phoneNumber = await findCustomerPhoneNumber(customerId);
|
const phoneNumber = await findCustomerPhoneNumber(customerId);
|
||||||
|
|
||||||
const messagesSent = await twilio(customer.accountSid, customer.authToken)
|
const [messagesSent, messagesReceived] = await Promise.all([
|
||||||
.messages
|
twilio(customer.accountSid, customer.authToken)
|
||||||
.list({ from: phoneNumber.phoneNumber });
|
.messages
|
||||||
const messagesReceived = await twilio(customer.accountSid, customer.authToken)
|
.list({ from: phoneNumber.phoneNumber }),
|
||||||
.messages
|
twilio(customer.accountSid, customer.authToken)
|
||||||
.list({ to: phoneNumber.phoneNumber });
|
.messages
|
||||||
|
.list({ to: phoneNumber.phoneNumber }),
|
||||||
|
]);
|
||||||
const messages = [
|
const messages = [
|
||||||
...messagesSent,
|
...messagesSent,
|
||||||
...messagesReceived,
|
...messagesReceived,
|
||||||
|
32
src/pages/api/queue/insert-calls.ts
Normal file
32
src/pages/api/queue/insert-calls.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Queue } from "quirrel/next";
|
||||||
|
import type { CallInstance } from "twilio/lib/rest/api/v2010/account/call";
|
||||||
|
|
||||||
|
import type { PhoneCall } from "../../../database/phone-call";
|
||||||
|
import { insertManyPhoneCalls } from "../../../database/phone-call";
|
||||||
|
|
||||||
|
type Payload = {
|
||||||
|
customerId: string;
|
||||||
|
calls: CallInstance[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertCallsQueue = Queue<Payload>(
|
||||||
|
"api/queue/insert-calls",
|
||||||
|
async ({ calls, customerId }) => {
|
||||||
|
const phoneCalls = calls
|
||||||
|
.map<Omit<PhoneCall, "id">>(call => ({
|
||||||
|
customerId,
|
||||||
|
twilioSid: call.sid,
|
||||||
|
from: call.from,
|
||||||
|
to: call.to,
|
||||||
|
direction: call.direction === "inbound" ? "inbound" : "outbound",
|
||||||
|
status: call.status,
|
||||||
|
duration: call.duration,
|
||||||
|
createdAt: new Date(call.dateCreated).toISOString(),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
||||||
|
|
||||||
|
await insertManyPhoneCalls(phoneCalls);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default insertCallsQueue;
|
@ -2,9 +2,8 @@ import { Queue } from "quirrel/next";
|
|||||||
import type { MessageInstance } from "twilio/lib/rest/api/v2010/account/message";
|
import type { MessageInstance } from "twilio/lib/rest/api/v2010/account/message";
|
||||||
|
|
||||||
import { findCustomer } from "../../../database/customer";
|
import { findCustomer } from "../../../database/customer";
|
||||||
import type { Sms } from "../../../database/_types";
|
import type { Message } from "../../../database/message";
|
||||||
import { SmsType } from "../../../database/_types";
|
import { insertManyMessage } from "../../../database/message";
|
||||||
import { insertManySms } from "../../../database/sms";
|
|
||||||
import { encrypt } from "../../../database/_encryption";
|
import { encrypt } from "../../../database/_encryption";
|
||||||
|
|
||||||
type Payload = {
|
type Payload = {
|
||||||
@ -18,16 +17,20 @@ const insertMessagesQueue = Queue<Payload>(
|
|||||||
const customer = await findCustomer(customerId);
|
const customer = await findCustomer(customerId);
|
||||||
const encryptionKey = customer.encryptionKey;
|
const encryptionKey = customer.encryptionKey;
|
||||||
|
|
||||||
const sms = messages.map<Omit<Sms, "id">>(message => ({
|
const sms = messages
|
||||||
customerId,
|
.map<Omit<Message, "id">>(message => ({
|
||||||
content: encrypt(message.body, encryptionKey),
|
customerId,
|
||||||
from: message.from,
|
content: encrypt(message.body, encryptionKey),
|
||||||
to: message.to,
|
from: message.from,
|
||||||
type: ["received", "receiving"].includes(message.status) ? SmsType.RECEIVED : SmsType.SENT,
|
to: message.to,
|
||||||
messageSid: message.sid,
|
status: message.status,
|
||||||
sentAt: message.dateSent.toISOString(),
|
direction: message.direction === "inbound" ? "inbound" : "outbound",
|
||||||
}));
|
twilioSid: message.sid,
|
||||||
await insertManySms(sms);
|
sentAt: new Date(message.dateSent).toISOString(),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.sentAt.localeCompare(b.sentAt));
|
||||||
|
|
||||||
|
await insertManyMessage(sms);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import twilio from "twilio";
|
|||||||
|
|
||||||
import { findCustomer } from "../../../database/customer";
|
import { findCustomer } from "../../../database/customer";
|
||||||
import { findCustomerPhoneNumber } from "../../../database/phone-number";
|
import { findCustomerPhoneNumber } from "../../../database/phone-number";
|
||||||
import { setTwilioSid } from "../../../database/sms";
|
import { setTwilioSid } from "../../../database/message";
|
||||||
|
|
||||||
type Payload = {
|
type Payload = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -9,14 +9,14 @@ type Payload = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const setTwilioWebhooks = Queue<Payload>(
|
const setTwilioWebhooks = Queue<Payload>(
|
||||||
"api/queue/send-message",
|
"api/queue/set-twilio-webhooks",
|
||||||
async ({ customerId }) => {
|
async ({ customerId }) => {
|
||||||
const customer = await findCustomer(customerId);
|
const customer = await findCustomer(customerId);
|
||||||
const twimlApp = await twilio(customer.accountSid, customer.authToken)
|
const twimlApp = await twilio(customer.accountSid, customer.authToken)
|
||||||
.applications
|
.applications
|
||||||
.create({
|
.create({
|
||||||
friendlyName: "Virtual Phone",
|
friendlyName: "Virtual Phone",
|
||||||
smsUrl: "https://phone.mokhtar.dev/api/webhook/incoming-sms",
|
smsUrl: "https://phone.mokhtar.dev/api/webhook/incoming-message",
|
||||||
smsMethod: "POST",
|
smsMethod: "POST",
|
||||||
voiceUrl: "https://phone.mokhtar.dev/api/webhook/incoming-call",
|
voiceUrl: "https://phone.mokhtar.dev/api/webhook/incoming-call",
|
||||||
voiceMethod: "POST",
|
voiceMethod: "POST",
|
||||||
|
@ -7,6 +7,7 @@ import appLogger from "../../../../lib/logger";
|
|||||||
import { createPhoneNumber } from "../../../database/phone-number";
|
import { createPhoneNumber } from "../../../database/phone-number";
|
||||||
import { findCustomer } from "../../../database/customer";
|
import { findCustomer } from "../../../database/customer";
|
||||||
import fetchMessagesQueue from "../queue/fetch-messages";
|
import fetchMessagesQueue from "../queue/fetch-messages";
|
||||||
|
import fetchCallsQueue from "../queue/fetch-calls";
|
||||||
import setTwilioWebhooks from "../queue/set-twilio-webhooks";
|
import setTwilioWebhooks from "../queue/set-twilio-webhooks";
|
||||||
|
|
||||||
const logger = appLogger.child({ route: "/api/user/add-phone-number" });
|
const logger = appLogger.child({ route: "/api/user/add-phone-number" });
|
||||||
@ -49,6 +50,7 @@ export default withApiAuthRequired(async function addPhoneNumberHandler(req, res
|
|||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
fetchMessagesQueue.enqueue({ customerId }, { id: `fetch-messages-${customerId}` }),
|
fetchMessagesQueue.enqueue({ customerId }, { id: `fetch-messages-${customerId}` }),
|
||||||
|
fetchCallsQueue.enqueue({ customerId }, { id: `fetch-messages-${customerId}` }),
|
||||||
setTwilioWebhooks.enqueue({ customerId }, { id: `set-twilio-webhooks-${customerId}` }),
|
setTwilioWebhooks.enqueue({ customerId }, { id: `set-twilio-webhooks-${customerId}` }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -3,14 +3,13 @@ import twilio from "twilio";
|
|||||||
|
|
||||||
import type { ApiError } from "../_types";
|
import type { ApiError } from "../_types";
|
||||||
import appLogger from "../../../../lib/logger";
|
import appLogger from "../../../../lib/logger";
|
||||||
import { Customer, findCustomerByPhoneNumber } from "../../../database/customer";
|
import { findCustomerByPhoneNumber } from "../../../database/customer";
|
||||||
import { insertSms } from "../../../database/sms";
|
import { insertMessage } from "../../../database/message";
|
||||||
import { SmsType } from "../../../database/_types";
|
|
||||||
import { encrypt } from "../../../database/_encryption";
|
import { encrypt } from "../../../database/_encryption";
|
||||||
|
|
||||||
const logger = appLogger.child({ route: "/api/webhook/incoming-sms" });
|
const logger = appLogger.child({ route: "/api/webhook/incoming-message" });
|
||||||
|
|
||||||
export default async function incomingSmsHandler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function incomingMessageHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (req.method !== "POST") {
|
if (req.method !== "POST") {
|
||||||
const statusCode = 405;
|
const statusCode = 405;
|
||||||
const apiError: ApiError = {
|
const apiError: ApiError = {
|
||||||
@ -41,7 +40,7 @@ export default async function incomingSmsHandler(req: NextApiRequest, res: NextA
|
|||||||
try {
|
try {
|
||||||
const phoneNumber = req.body.To;
|
const phoneNumber = req.body.To;
|
||||||
const customer = await findCustomerByPhoneNumber(phoneNumber);
|
const customer = await findCustomerByPhoneNumber(phoneNumber);
|
||||||
const url = "https://phone.mokhtar.dev/api/webhook/incoming-sms";
|
const url = "https://phone.mokhtar.dev/api/webhook/incoming-message";
|
||||||
const isRequestValid = twilio.validateRequest(customer.authToken!, twilioSignature, url, req.body);
|
const isRequestValid = twilio.validateRequest(customer.authToken!, twilioSignature, url, req.body);
|
||||||
if (!isRequestValid) {
|
if (!isRequestValid) {
|
||||||
const statusCode = 400;
|
const statusCode = 400;
|
||||||
@ -55,11 +54,12 @@ export default async function incomingSmsHandler(req: NextApiRequest, res: NextA
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await insertSms({
|
await insertMessage({
|
||||||
customerId: customer.id,
|
customerId: customer.id,
|
||||||
to: req.body.To,
|
to: req.body.To,
|
||||||
from: req.body.From,
|
from: req.body.From,
|
||||||
type: SmsType.RECEIVED,
|
status: "received",
|
||||||
|
direction: "inbound",
|
||||||
sentAt: req.body.DateSent,
|
sentAt: req.body.DateSent,
|
||||||
content: encrypt(req.body.Body, customer.encryptionKey),
|
content: encrypt(req.body.Body, customer.encryptionKey),
|
||||||
});
|
});
|
@ -1,14 +1,15 @@
|
|||||||
import type { InferGetServerSidePropsType, NextPage } from "next";
|
import type { InferGetServerSidePropsType, NextPage } from "next";
|
||||||
|
|
||||||
import { withPageOnboardingRequired } from "../../lib/session-helpers";
|
import { withPageOnboardingRequired } from "../../lib/session-helpers";
|
||||||
import Layout from "../components/layout";
|
import { findCustomerPhoneCalls } from "../database/phone-call";
|
||||||
import useUser from "../hooks/use-user";
|
import useUser from "../hooks/use-user";
|
||||||
|
import Layout from "../components/layout";
|
||||||
|
|
||||||
type Props = InferGetServerSidePropsType<typeof getServerSideProps>;
|
type Props = InferGetServerSidePropsType<typeof getServerSideProps>;
|
||||||
|
|
||||||
const pageTitle = "Calls";
|
const pageTitle = "Calls";
|
||||||
|
|
||||||
const Calls: NextPage<Props> = (props) => {
|
const Calls: NextPage<Props> = ({ phoneCalls }) => {
|
||||||
const { userProfile } = useUser();
|
const { userProfile } = useUser();
|
||||||
|
|
||||||
console.log("userProfile", userProfile);
|
console.log("userProfile", userProfile);
|
||||||
@ -21,19 +22,34 @@ const Calls: NextPage<Props> = (props) => {
|
|||||||
<Layout title={pageTitle}>
|
<Layout title={pageTitle}>
|
||||||
<div className="flex flex-col space-y-6 p-6">
|
<div className="flex flex-col space-y-6 p-6">
|
||||||
<p>Calls page</p>
|
<p>Calls page</p>
|
||||||
|
<ul className="divide-y">
|
||||||
|
{phoneCalls.map((phoneCall) => {
|
||||||
|
const recipient = phoneCall.direction === "outbound" ? phoneCall.to : phoneCall.from;
|
||||||
|
return (
|
||||||
|
<li key={phoneCall.twilioSid} className="flex flex-row justify-between py-2">
|
||||||
|
<div>{recipient}</div>
|
||||||
|
<div>{new Date(phoneCall.createdAt).toLocaleString("fr-FR")}</div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getServerSideProps = withPageOnboardingRequired(
|
export const getServerSideProps = withPageOnboardingRequired(
|
||||||
async ({ res }) => {
|
async ({ res }, user) => {
|
||||||
res.setHeader(
|
res.setHeader(
|
||||||
"Cache-Control",
|
"Cache-Control",
|
||||||
"private, s-maxage=15, stale-while-revalidate=59",
|
"private, s-maxage=15, stale-while-revalidate=59",
|
||||||
);
|
);
|
||||||
|
|
||||||
return { props: {} };
|
const phoneCalls = await findCustomerPhoneCalls(user.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: { phoneCalls },
|
||||||
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -7,9 +7,8 @@ import clsx from "clsx";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { withPageOnboardingRequired } from "../../../lib/session-helpers";
|
import { withPageOnboardingRequired } from "../../../lib/session-helpers";
|
||||||
import { findConversation } from "../../database/sms";
|
import type { Message } from "../../database/message";
|
||||||
import type { Sms } from "../../database/_types";
|
import { findConversation } from "../../database/message";
|
||||||
import { SmsType } from "../../database/_types";
|
|
||||||
import supabase from "../../supabase/client";
|
import supabase from "../../supabase/client";
|
||||||
import useUser from "../../hooks/use-user";
|
import useUser from "../../hooks/use-user";
|
||||||
import useConversation from "../../hooks/use-conversation";
|
import useConversation from "../../hooks/use-conversation";
|
||||||
@ -17,7 +16,7 @@ import Layout from "../../components/layout";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
recipient: string;
|
recipient: string;
|
||||||
conversation: Sms[];
|
conversation: Message[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type Form = {
|
type Form = {
|
||||||
@ -60,7 +59,7 @@ const Messages: NextPage<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const subscription = supabase
|
const subscription = supabase
|
||||||
.from<Sms>(`sms:customerId=eq.${userProfile.id}`)
|
.from<Message>(`sms:customerId=eq.${userProfile.id}`)
|
||||||
.on("INSERT", (payload) => {
|
.on("INSERT", (payload) => {
|
||||||
const message = payload.new;
|
const message = payload.new;
|
||||||
if ([message.from, message.to].includes(recipient)) {
|
if ([message.from, message.to].includes(recipient)) {
|
||||||
@ -98,12 +97,28 @@ const Messages: NextPage<Props> = (props) => {
|
|||||||
</header>
|
</header>
|
||||||
<div className="flex flex-col space-y-6 p-6">
|
<div className="flex flex-col space-y-6 p-6">
|
||||||
<ul>
|
<ul>
|
||||||
{conversation!.map(message => {
|
{conversation!.map((message, index) => {
|
||||||
|
const isOutbound = message.direction === "outbound";
|
||||||
|
const isSameSender = message.from === conversation![index + 1]?.from;
|
||||||
|
const isLast = index === conversation!.length;
|
||||||
return (
|
return (
|
||||||
<li key={message.id} className={clsx(message.type === SmsType.SENT ? "text-right" : "text-left")}>
|
<li
|
||||||
{message.content}
|
key={message.id}
|
||||||
|
className={clsx(
|
||||||
|
isSameSender || isLast ? "pb-3" : "pb-4",
|
||||||
|
isOutbound ? "text-right" : "text-left",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={clsx(
|
||||||
|
"p-2 rounded-lg text-white",
|
||||||
|
isOutbound ? "bg-[#3194ff] rounded-br-none" : "bg-black rounded-bl-none",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{message.content}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,9 +2,8 @@ import type { InferGetServerSidePropsType, NextPage } from "next";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import { withPageOnboardingRequired } from "../../../lib/session-helpers";
|
import { withPageOnboardingRequired } from "../../../lib/session-helpers";
|
||||||
import type { Sms } from "../../database/_types";
|
import type { Message } from "../../database/message";
|
||||||
import { SmsType } from "../../database/_types";
|
import { findCustomerMessages } from "../../database/message";
|
||||||
import { findCustomerMessages } from "../../database/sms";
|
|
||||||
import { findCustomer } from "../../database/customer";
|
import { findCustomer } from "../../database/customer";
|
||||||
import { decrypt } from "../../database/_encryption";
|
import { decrypt } from "../../database/_encryption";
|
||||||
import useUser from "../../hooks/use-user";
|
import useUser from "../../hooks/use-user";
|
||||||
@ -25,15 +24,17 @@ const Messages: NextPage<Props> = ({ conversations }) => {
|
|||||||
<Layout title={pageTitle}>
|
<Layout title={pageTitle}>
|
||||||
<div className="flex flex-col space-y-6 p-6">
|
<div className="flex flex-col space-y-6 p-6">
|
||||||
<p>Messages page</p>
|
<p>Messages page</p>
|
||||||
<ul>
|
<ul className="divide-y">
|
||||||
{Object.entries(conversations).map(([recipient, message]) => {
|
{Object.entries(conversations).map(([recipient, message]) => {
|
||||||
return (
|
return (
|
||||||
<li key={recipient}>
|
<li key={recipient} className="py-2">
|
||||||
<Link href={`/messages/${recipient}`}>
|
<Link href={`/messages/${recipient}`}>
|
||||||
<a>
|
<a className="flex flex-col">
|
||||||
<div>{recipient}</div>
|
<div className="flex flex-row justify-between">
|
||||||
|
<strong>{recipient}</strong>
|
||||||
|
<div>{new Date(message.sentAt).toLocaleString("fr-FR")}</div>
|
||||||
|
</div>
|
||||||
<div>{message.content}</div>
|
<div>{message.content}</div>
|
||||||
<div>{new Date(message.sentAt).toLocaleDateString()}</div>
|
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
@ -59,10 +60,10 @@ export const getServerSideProps = withPageOnboardingRequired(
|
|||||||
findCustomerMessages(user.id),
|
findCustomerMessages(user.id),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let conversations: Record<Recipient, Sms> = {};
|
let conversations: Record<Recipient, Message> = {};
|
||||||
for (const message of messages) {
|
for (const message of messages) {
|
||||||
let recipient: string;
|
let recipient: string;
|
||||||
if (message.type === SmsType.SENT) {
|
if (message.direction === "outbound") {
|
||||||
recipient = message.to;
|
recipient = message.to;
|
||||||
} else {
|
} else {
|
||||||
recipient = message.from;
|
recipient = message.from;
|
||||||
|
Loading…
Reference in New Issue
Block a user