migrate to blitzjs
This commit is contained in:
4
app/api/_types.ts
Normal file
4
app/api/_types.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type ApiError = {
|
||||
statusCode: number
|
||||
errorMessage: string
|
||||
}
|
16
app/api/ddd.ts
Normal file
16
app/api/ddd.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { BlitzApiRequest, BlitzApiResponse } from "blitz"
|
||||
|
||||
import db from "db"
|
||||
|
||||
export default async function ddd(req: BlitzApiRequest, res: BlitzApiResponse) {
|
||||
await Promise.all([
|
||||
db.message.deleteMany(),
|
||||
db.phoneCall.deleteMany(),
|
||||
db.phoneNumber.deleteMany(),
|
||||
db.customer.deleteMany(),
|
||||
])
|
||||
|
||||
await db.user.deleteMany()
|
||||
|
||||
res.status(200).end()
|
||||
}
|
21
app/api/newsletter/_mailchimp.ts
Normal file
21
app/api/newsletter/_mailchimp.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import getConfig from "next/config"
|
||||
import axios from "axios"
|
||||
|
||||
const { serverRuntimeConfig } = getConfig()
|
||||
|
||||
export async function addSubscriber(email: string) {
|
||||
const { apiKey, audienceId } = serverRuntimeConfig.mailChimp
|
||||
const region = apiKey.split("-")[1]
|
||||
const url = `https://${region}.api.mailchimp.com/3.0/lists/${audienceId}/members`
|
||||
const data = {
|
||||
email_address: email,
|
||||
status: "subscribed",
|
||||
}
|
||||
const base64ApiKey = Buffer.from(`any:${apiKey}`).toString("base64")
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Basic ${base64ApiKey}`,
|
||||
}
|
||||
|
||||
return axios.post(url, data, { headers })
|
||||
}
|
59
app/api/newsletter/subscribe.ts
Normal file
59
app/api/newsletter/subscribe.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next"
|
||||
import zod from "zod"
|
||||
|
||||
import type { ApiError } from "../_types"
|
||||
import appLogger from "../../../integrations/logger"
|
||||
import { addSubscriber } from "./_mailchimp"
|
||||
|
||||
type Response = {} | ApiError
|
||||
|
||||
const logger = appLogger.child({ route: "/api/newsletter/subscribe" })
|
||||
|
||||
const bodySchema = zod.object({
|
||||
email: zod.string().email(),
|
||||
})
|
||||
|
||||
export default async function subscribeToNewsletter(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<Response>
|
||||
) {
|
||||
if (req.method !== "POST") {
|
||||
const statusCode = 405
|
||||
const apiError: ApiError = {
|
||||
statusCode,
|
||||
errorMessage: `Method ${req.method} Not Allowed`,
|
||||
}
|
||||
logger.error(apiError)
|
||||
|
||||
res.setHeader("Allow", ["POST"])
|
||||
res.status(statusCode).send(apiError)
|
||||
return
|
||||
}
|
||||
|
||||
let body
|
||||
try {
|
||||
body = bodySchema.parse(req.body)
|
||||
} catch (error) {
|
||||
const statusCode = 400
|
||||
const apiError: ApiError = {
|
||||
statusCode,
|
||||
errorMessage: "Body is malformed",
|
||||
}
|
||||
logger.error(error)
|
||||
|
||||
res.status(statusCode).send(apiError)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await addSubscriber(body.email)
|
||||
} catch (error) {
|
||||
console.log("error", error.response?.data)
|
||||
|
||||
if (error.response?.data.title !== "Member Exists") {
|
||||
return res.status(error.response?.status ?? 400).end()
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).end()
|
||||
}
|
38
app/api/queue/fetch-calls.ts
Normal file
38
app/api/queue/fetch-calls.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { Queue } from "quirrel/blitz"
|
||||
import twilio from "twilio"
|
||||
|
||||
import db from "../../../db"
|
||||
import insertCallsQueue from "./insert-calls"
|
||||
|
||||
type Payload = {
|
||||
customerId: string
|
||||
}
|
||||
|
||||
const fetchCallsQueue = Queue<Payload>("api/queue/fetch-calls", async ({ customerId }) => {
|
||||
const customer = await db.customer.findFirst({ where: { id: customerId } })
|
||||
const phoneNumber = await db.phoneNumber.findFirst({ where: { 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
|
38
app/api/queue/fetch-messages.ts
Normal file
38
app/api/queue/fetch-messages.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { Queue } from "quirrel/blitz"
|
||||
import twilio from "twilio"
|
||||
|
||||
import db from "../../../db"
|
||||
import insertMessagesQueue from "./insert-messages"
|
||||
|
||||
type Payload = {
|
||||
customerId: string
|
||||
}
|
||||
|
||||
const fetchMessagesQueue = Queue<Payload>("api/queue/fetch-messages", async ({ customerId }) => {
|
||||
const customer = await db.customer.findFirst({ where: { id: customerId } })
|
||||
const phoneNumber = await db.phoneNumber.findFirst({ where: { customerId } })
|
||||
|
||||
const [messagesSent, messagesReceived] = await Promise.all([
|
||||
twilio(customer!.accountSid!, customer!.authToken!).messages.list({
|
||||
from: phoneNumber!.phoneNumber,
|
||||
}),
|
||||
twilio(customer!.accountSid!, customer!.authToken!).messages.list({
|
||||
to: phoneNumber!.phoneNumber,
|
||||
}),
|
||||
])
|
||||
const messages = [...messagesSent, ...messagesReceived].sort(
|
||||
(a, b) => a.dateSent.getTime() - b.dateSent.getTime()
|
||||
)
|
||||
|
||||
await insertMessagesQueue.enqueue(
|
||||
{
|
||||
customerId,
|
||||
messages,
|
||||
},
|
||||
{
|
||||
id: `insert-messages-${customerId}`,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
export default fetchMessagesQueue
|
59
app/api/queue/insert-calls.ts
Normal file
59
app/api/queue/insert-calls.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { Queue } from "quirrel/blitz"
|
||||
import type { CallInstance } from "twilio/lib/rest/api/v2010/account/call"
|
||||
|
||||
import db, { Direction, CallStatus } from "../../../db"
|
||||
|
||||
type Payload = {
|
||||
customerId: string
|
||||
calls: CallInstance[]
|
||||
}
|
||||
|
||||
const insertCallsQueue = Queue<Payload>("api/queue/insert-calls", async ({ calls, customerId }) => {
|
||||
const phoneCalls = calls
|
||||
.map((call) => ({
|
||||
customerId,
|
||||
twilioSid: call.sid,
|
||||
from: call.from,
|
||||
to: call.to,
|
||||
direction: translateDirection(call.direction),
|
||||
status: translateStatus(call.status),
|
||||
duration: call.duration,
|
||||
createdAt: new Date(call.dateCreated),
|
||||
}))
|
||||
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())
|
||||
|
||||
await db.phoneCall.createMany({ data: phoneCalls })
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
}
|
78
app/api/queue/insert-messages.ts
Normal file
78
app/api/queue/insert-messages.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { Queue } from "quirrel/blitz"
|
||||
import type { MessageInstance } from "twilio/lib/rest/api/v2010/account/message"
|
||||
|
||||
import db, { MessageStatus, Direction, Message } from "../../../db"
|
||||
import { encrypt } from "../../../db/_encryption"
|
||||
|
||||
type Payload = {
|
||||
customerId: string
|
||||
messages: MessageInstance[]
|
||||
}
|
||||
|
||||
const insertMessagesQueue = Queue<Payload>(
|
||||
"api/queue/insert-messages",
|
||||
async ({ messages, customerId }) => {
|
||||
const customer = await db.customer.findFirst({ where: { id: customerId } })
|
||||
const encryptionKey = customer!.encryptionKey
|
||||
|
||||
const sms = messages
|
||||
.map<Omit<Message, "id">>((message) => ({
|
||||
customerId,
|
||||
content: encrypt(message.body, encryptionKey),
|
||||
from: message.from,
|
||||
to: message.to,
|
||||
status: translateStatus(message.status),
|
||||
direction: translateDirection(message.direction),
|
||||
twilioSid: message.sid,
|
||||
sentAt: new Date(message.dateSent),
|
||||
}))
|
||||
.sort((a, b) => a.sentAt.getTime() - b.sentAt.getTime())
|
||||
|
||||
await db.message.createMany({ data: sms })
|
||||
}
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
34
app/api/queue/send-message.ts
Normal file
34
app/api/queue/send-message.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { Queue } from "quirrel/blitz"
|
||||
import twilio from "twilio"
|
||||
|
||||
import db from "../../../db"
|
||||
|
||||
type Payload = {
|
||||
id: string
|
||||
customerId: string
|
||||
to: string
|
||||
content: string
|
||||
}
|
||||
|
||||
const sendMessageQueue = Queue<Payload>(
|
||||
"api/queue/send-message",
|
||||
async ({ id, customerId, to, content }) => {
|
||||
const customer = await db.customer.findFirst({ where: { id: customerId } })
|
||||
const phoneNumber = await db.phoneNumber.findFirst({ where: { customerId } })
|
||||
|
||||
const message = await twilio(customer!.accountSid!, customer!.authToken!).messages.create({
|
||||
body: content,
|
||||
to,
|
||||
from: phoneNumber!.phoneNumber,
|
||||
})
|
||||
await db.message.update({
|
||||
where: { id },
|
||||
data: { twilioSid: message.sid },
|
||||
})
|
||||
},
|
||||
{
|
||||
retry: ["1min"],
|
||||
}
|
||||
)
|
||||
|
||||
export default sendMessageQueue
|
43
app/api/queue/set-twilio-webhooks.ts
Normal file
43
app/api/queue/set-twilio-webhooks.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { Queue } from "quirrel/blitz"
|
||||
import twilio from "twilio"
|
||||
|
||||
import db from "../../../db"
|
||||
|
||||
type Payload = {
|
||||
customerId: string
|
||||
}
|
||||
|
||||
const setTwilioWebhooks = Queue<Payload>(
|
||||
"api/queue/set-twilio-webhooks",
|
||||
async ({ customerId }) => {
|
||||
const customer = await db.customer.findFirst({ where: { id: customerId } })
|
||||
const twimlApp = customer!.twimlAppSid
|
||||
? await twilio(customer!.accountSid!, customer!.authToken!)
|
||||
.applications.get(customer!.twimlAppSid)
|
||||
.fetch()
|
||||
: await twilio(customer!.accountSid!, customer!.authToken!).applications.create({
|
||||
friendlyName: "Virtual Phone",
|
||||
smsUrl: "https://phone.mokhtar.dev/api/webhook/incoming-message",
|
||||
smsMethod: "POST",
|
||||
voiceUrl: "https://phone.mokhtar.dev/api/webhook/incoming-call",
|
||||
voiceMethod: "POST",
|
||||
})
|
||||
const twimlAppSid = twimlApp.sid
|
||||
const phoneNumber = await db.phoneNumber.findFirst({ where: { customerId } })
|
||||
|
||||
await Promise.all([
|
||||
db.customer.update({
|
||||
where: { id: customerId },
|
||||
data: { twimlAppSid },
|
||||
}),
|
||||
twilio(customer!.accountSid!, customer!.authToken!)
|
||||
.incomingPhoneNumbers.get(phoneNumber!.phoneNumberSid)
|
||||
.update({
|
||||
smsApplicationSid: twimlAppSid,
|
||||
voiceApplicationSid: twimlAppSid,
|
||||
}),
|
||||
])
|
||||
}
|
||||
)
|
||||
|
||||
export default setTwilioWebhooks
|
Reference in New Issue
Block a user