diff --git a/.env.example b/.env.example index c7cf118..b58c0ab 100644 --- a/.env.example +++ b/.env.example @@ -19,3 +19,7 @@ RAZORPAY_SECRET="" RAZORPAY_WEBHOOK_SECRET="" UPLOADTHING_TOKEN="" + +TWILIO_ACCOUNT_SID="" +TWILIO_AUTH_TOKEN="" +TWILIO_WHATSAPP_NUMBER="" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1ebca79..363f4b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "node-html-to-image": "^5.0.0", "nodemailer": "^6.9.16", "razorpay": "^2.9.5", + "twilio": "^5.4.0", "uploadthing": "^7.3.0", "uuid": "^11.0.3", "zod": "^3.23.8" @@ -2405,6 +2406,12 @@ "node": ">= 14" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.3.7", "license": "MIT", @@ -4933,6 +4940,12 @@ "version": "2.1.2", "license": "MIT" }, + "node_modules/scmp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", + "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==", + "license": "BSD-3-Clause" + }, "node_modules/semver": { "version": "7.6.3", "license": "ISC", @@ -5371,6 +5384,49 @@ "fsevents": "~2.3.3" } }, + "node_modules/twilio": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/twilio/-/twilio-5.4.0.tgz", + "integrity": "sha512-kEmxzdOLTzXzUEXIkBVwT1Itxlbp+rtGrQogNfPtSE3EjoEsxrxB/9tdMIEbrsioL8CzTk/+fiKNJekAyHxjuQ==", + "license": "MIT", + "dependencies": { + "axios": "^1.7.4", + "dayjs": "^1.11.9", + "https-proxy-agent": "^5.0.0", + "jsonwebtoken": "^9.0.2", + "qs": "^6.9.4", + "scmp": "^2.1.0", + "xmlbuilder": "^13.0.2" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/twilio/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/twilio/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/type-check": { "version": "0.4.0", "dev": true, @@ -5662,6 +5718,15 @@ } } }, + "node_modules/xmlbuilder": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", + "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "license": "ISC", diff --git a/package.json b/package.json index c02dbbc..23d0262 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "node-html-to-image": "^5.0.0", "nodemailer": "^6.9.16", "razorpay": "^2.9.5", + "twilio": "^5.4.0", "uploadthing": "^7.3.0", "uuid": "^11.0.3", "zod": "^3.23.8" diff --git a/src/env.ts b/src/env.ts index 748d424..8e77bc1 100644 --- a/src/env.ts +++ b/src/env.ts @@ -34,6 +34,9 @@ export const env = createEnv({ RAZORPAY_SECRET: z.string(), RAZORPAY_WEBHOOK_SECRET: z.string(), UPLOADTHING_TOKEN: z.string(), + TWILIO_ACCOUNT_SID: z.string(), + TWILIO_AUTH_TOKEN: z.string(), + TWILIO_WHATSAPP_NUMBER: z.string(), }, /** @@ -73,6 +76,9 @@ export const env = createEnv({ RAZORPAY_SECRET: process.env.RAZORPAY_SECRET, RAZORPAY_WEBHOOK_SECRET: process.env.RAZORPAY_WEBHOOK_SECRET, UPLOADTHING_TOKEN: process.env.UPLOADTHING_TOKEN, + TWILIO_ACCOUNT_SID: process.env.TWILIO_ACCOUNT_SID, + TWILIO_AUTH_TOKEN: process.env.TWILIO_AUTH_TOKEN, + TWILIO_WHATSAPP_NUMBER: process.env.TWILIO_WHATSAPP_NUMBER, }, /** * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially diff --git a/src/graphql/models/Twilio/mutation.ts b/src/graphql/models/Twilio/mutation.ts new file mode 100644 index 0000000..6e4bb3d --- /dev/null +++ b/src/graphql/models/Twilio/mutation.ts @@ -0,0 +1,128 @@ +import { builder } from "~/graphql/builder"; +import { sendWhatsAppMessage } from "~/services/twilio.service"; + +builder.mutationField("sendEventRegistrationReminder", (t) => + t.field({ + type: "Boolean", + args: { + eventId: t.arg.id({ required: true }), + }, + resolve: async (root, args, ctx) => { + try { + const user = await ctx.user; + if (!user) { + throw new Error("Not authenticated"); + } + if (user.role !== "ORGANIZER") { + throw new Error("Not authorized to send WhatsApp notifications"); + } + + const event = await ctx.prisma.event.findUnique({ + where: { id: Number(args.eventId) }, + include: { + Teams: { + include: { + TeamMembers: { + include: { + User: true, + }, + }, + }, + }, + }, + }); + + if (!event) { + throw new Error("Event not found"); + } + + for (const team of event.Teams) { + for (const member of team.TeamMembers) { + const contentVariables = JSON.stringify({ + "1": member.User.name, + "2": event.name, + }); + await sendWhatsAppMessage( + member.User.phoneNumber!, + "HX154ff0082b3bd18082e22c5301927562", + contentVariables, + ); + } + } + return true; + } catch (error) { + console.error( + "Error in sendEventRegistrationReminder mutation:", + error, + ); + throw new Error(`Unexpected error: ${(error as Error).message}`); + } + }, + }), +); + +builder.mutationField("sendWinnerWhatsAppNotification", (t) => + t.field({ + type: "Boolean", + args: { + eventId: t.arg.id({ required: true }), + }, + resolve: async (root, args, ctx) => { + try { + const user = await ctx.user; + if (!user) { + throw new Error("Not authenticated"); + } + if (user.role !== "ORGANIZER") { + throw new Error("Not authorized to send WhatsApp notifications"); + } + + const event = await ctx.prisma.event.findUnique({ + where: { id: Number(args.eventId) }, + include: { + Winner: { + include: { + Team: { + include: { + TeamMembers: { + include: { + User: true, + }, + }, + }, + }, + }, + }, + }, + }); + + if (!event) { + throw new Error("Event not found"); + } + + for (const winner of event.Winner) { + for (const member of winner.Team.TeamMembers) { + const contentVariables = JSON.stringify({ + "1": member.User.name, + "2": winner.Team.name, + "3": winner.type, + "4": event.name, + }); + await sendWhatsAppMessage( + member.User.phoneNumber!, + "HX902d67cec04bbd3d3f9f0eb0c64a752a", + contentVariables, + ); + } + } + return true; + } catch (error) { + console.error( + "Error in sendWinnerWhatsAppNotification mutation:", + error, + ); + throw new Error(`Unexpected error: ${(error as Error).message}`); + } + }, + }), +); diff --git a/src/graphql/models/index.ts b/src/graphql/models/index.ts index 30b9eba..1c33948 100644 --- a/src/graphql/models/index.ts +++ b/src/graphql/models/index.ts @@ -24,3 +24,4 @@ import "~/graphql/models/User"; import "~/graphql/models/UserInHotel"; import "~/graphql/models/Winner"; import "~/graphql/models/XP"; +import "~/graphql/models/Twilio/mutation"; diff --git a/src/services/twilio.service.ts b/src/services/twilio.service.ts new file mode 100644 index 0000000..d4102d9 --- /dev/null +++ b/src/services/twilio.service.ts @@ -0,0 +1,23 @@ +import Twilio from "twilio"; +import { env } from "~/env"; + +const client = Twilio(env.TWILIO_ACCOUNT_SID, env.TWILIO_AUTH_TOKEN); + +export const sendWhatsAppMessage = async ( + to: string, + contentSid: string, + contentVariables: string, +) => { + try { + const message = await client.messages.create({ + from: env.TWILIO_WHATSAPP_NUMBER, + contentSid, + contentVariables, + to: `whatsapp:${to}`, + }); + return message; + } catch (error) { + console.error("Error sending WhatsApp message:", error); + throw new Error("Failed to send WhatsApp message"); + } +};