implement chat functionality for users and hosts with message sending and retrieval

This commit is contained in:
paritosh18
2026-04-13 15:13:43 +05:30
parent c5dcc5b1f0
commit dcb2259c7d
8 changed files with 541 additions and 0 deletions

View File

@@ -79,6 +79,8 @@ model User {
// 🔹 Activities where this user is Account Manager
managedActivities Activities[] @relation("ActivityAccountManager")
activitySortings ActivitySorting[]
sentActivityMessages ActivityMessages[] @relation("ActivityMessageSender")
receivedActivityMessages ActivityMessages[] @relation("ActivityMessageReceiver")
@@map("users")
@@schema("usr")
@@ -1036,11 +1038,30 @@ model Activities {
activityCuisines ActivityCuisine[]
activityPickUpTransports ActivityPickUpTransport[]
userBucketInterests UserBucketInterested[]
activityMessages ActivityMessages[]
@@map("activities")
@@schema("act")
}
model ActivityMessages {
id Int @id @default(autoincrement())
activityXid Int @map("activity_xid")
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
senderXid Int @map("sender_xid")
sender User @relation("ActivityMessageSender", fields: [senderXid], references: [id], onDelete: Restrict)
receivedXid Int @map("received_xid")
received User @relation("ActivityMessageReceiver", fields: [receivedXid], references: [id], onDelete: Restrict)
message String @map("message") @db.VarChar(2000)
status String @default("unread") @map("status") @db.VarChar(30)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([activityXid, senderXid, receivedXid])
@@map("activity_messages")
@@schema("act")
}
model ActivityOtherDetails {
id Int @id @default(autoincrement())
activityXid Int @map("activity_xid")

View File

@@ -585,3 +585,33 @@ submitPQAnswer:
- httpApi:
path: /Activity_Hub/OnBoarding/submit-pq-answer
method: patch
sendHostChatMessage:
handler: src/modules/host/handlers/chat/sendMessage.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/chat/send-message
method: post
getHostChatMessages:
handler: src/modules/host/handlers/chat/getMessages.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/chat/messages
method: get

View File

@@ -557,3 +557,33 @@ getMatchingBucketInterestedActivities:
- httpApi:
path: /itinerary/get-matching-bucket-interested-activities
method: post
sendUserChatMessage:
handler: src/modules/user/handlers/chat/sendMessage.handler
memorySize: 384
package:
patterns:
- 'src/modules/user/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /chat/send-message
method: post
getUserChatMessages:
handler: src/modules/user/handlers/chat/getMessages.handler
memorySize: 384
package:
patterns:
- 'src/modules/user/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /chat/messages
method: get

View File

@@ -0,0 +1,150 @@
import { PrismaClient } from '@prisma/client';
import ApiError from '../utils/helper/ApiError';
interface SendMessageInput {
activityXid: number;
senderXid: number;
receiverXid: number;
message: string;
status?: string;
}
interface GetMessagesInput {
activityXid: number;
userXid: number;
otherUserXid: number;
limit?: number;
}
export class ChatService {
constructor(private prisma: PrismaClient) {}
private async getHostUserIdForActivity(activityXid: number): Promise<number> {
const activity = await this.prisma.activities.findUnique({
where: { id: activityXid },
select: { host: { select: { userXid: true } } },
});
if (!activity) {
throw new ApiError(404, 'Activity not found');
}
const hostUserXid = activity.host?.userXid;
if (!hostUserXid) {
throw new ApiError(400, 'Host user not found for activity');
}
return hostUserXid;
}
async sendMessage(input: SendMessageInput) {
if (!input.activityXid || isNaN(input.activityXid)) {
throw new ApiError(400, 'Valid activityXid is required');
}
if (!input.senderXid || isNaN(input.senderXid)) {
throw new ApiError(400, 'Valid senderXid is required');
}
if (!input.receiverXid || isNaN(input.receiverXid)) {
throw new ApiError(400, 'Valid receiverXid is required');
}
if (input.senderXid === input.receiverXid) {
throw new ApiError(400, 'Sender and receiver cannot be the same');
}
const message = input.message?.trim();
if (!message) {
throw new ApiError(400, 'Message is required');
}
const hostUserXid = await this.getHostUserIdForActivity(input.activityXid);
if (input.senderXid !== hostUserXid && input.receiverXid !== hostUserXid) {
throw new ApiError(
400,
'Sender or receiver must be the host for this activity'
);
}
const receiverExists = await this.prisma.user.findUnique({
where: { id: input.receiverXid },
select: { id: true },
});
if (!receiverExists) {
throw new ApiError(404, 'Receiver not found');
}
return this.prisma.activityMessages.create({
data: {
activityXid: input.activityXid,
senderXid: input.senderXid,
receivedXid: input.receiverXid,
message,
status: input.status?.trim() || 'unread',
},
});
}
async getMessages(input: GetMessagesInput) {
if (!input.activityXid || isNaN(input.activityXid)) {
throw new ApiError(400, 'Valid activityXid is required');
}
if (!input.userXid || isNaN(input.userXid)) {
throw new ApiError(400, 'Valid userXid is required');
}
if (!input.otherUserXid || isNaN(input.otherUserXid)) {
throw new ApiError(400, 'Valid otherUserXid is required');
}
if (input.userXid === input.otherUserXid) {
throw new ApiError(400, 'Invalid otherUserXid');
}
const hostUserXid = await this.getHostUserIdForActivity(input.activityXid);
if (input.userXid !== hostUserXid && input.otherUserXid !== hostUserXid) {
throw new ApiError(
400,
'Conversation must include the host for this activity'
);
}
const limit = Math.min(Math.max(input.limit || 50, 1), 200);
const messages = await this.prisma.activityMessages.findMany({
where: {
activityXid: input.activityXid,
OR: [
{
senderXid: input.userXid,
receivedXid: input.otherUserXid,
},
{
senderXid: input.otherUserXid,
receivedXid: input.userXid,
},
],
},
orderBy: { createdAt: 'asc' },
take: limit,
});
await this.prisma.activityMessages.updateMany({
where: {
activityXid: input.activityXid,
senderXid: input.otherUserXid,
receivedXid: input.userXid,
status: 'unread',
},
data: { status: 'read' },
});
return messages;
}
}

View File

@@ -0,0 +1,77 @@
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyHostToken } from '../../../../common/middlewares/jwt/authForHost';
import { ChatService } from '../../../../common/services/chat.service';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
const chatService = new ChatService(prismaClient);
export const handler = safeHandler(
async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
const token =
event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
400,
'This is a protected route. Please provide a valid token.'
);
}
const userInfo = await verifyHostToken(token);
const hostUserId = Number(userInfo.id);
if (!hostUserId || isNaN(hostUserId)) {
throw new ApiError(400, 'Invalid host user ID');
}
const activityXidParam =
event.queryStringParameters?.activityXid ||
event.queryStringParameters?.activity_xid;
const otherUserXidParam =
event.queryStringParameters?.otherUserXid ||
event.queryStringParameters?.other_user_xid ||
event.queryStringParameters?.userXid ||
event.queryStringParameters?.user_xid;
const limitParam = event.queryStringParameters?.limit;
const activityXid = Number(activityXidParam);
const otherUserXid = Number(otherUserXidParam);
const limit = limitParam ? Number(limitParam) : undefined;
if (!activityXid || isNaN(activityXid)) {
throw new ApiError(400, 'Valid activityXid is required');
}
if (!otherUserXid || isNaN(otherUserXid)) {
throw new ApiError(400, 'Valid otherUserXid is required');
}
const messages = await chatService.getMessages({
activityXid,
userXid: hostUserId,
otherUserXid,
limit,
});
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Messages retrieved successfully',
data: messages,
}),
};
}
);

View File

@@ -0,0 +1,78 @@
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyHostToken } from '../../../../common/middlewares/jwt/authForHost';
import { ChatService } from '../../../../common/services/chat.service';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
const chatService = new ChatService(prismaClient);
export const handler = safeHandler(
async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
const token =
event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
400,
'This is a protected route. Please provide a valid token.'
);
}
const userInfo = await verifyHostToken(token);
const hostUserId = Number(userInfo.id);
if (!hostUserId || isNaN(hostUserId)) {
throw new ApiError(400, 'Invalid host user ID');
}
let body: any;
try {
body = JSON.parse(event.body || '{}');
} catch {
throw new ApiError(400, 'Invalid JSON in request body');
}
const activityXid = Number(body.activityXid ?? body.activity_xid);
const receiverXid = Number(
body.receiverXid ?? body.receivedXid ?? body.received_xid
);
const message = body.message;
const status = body.status;
if (!activityXid || isNaN(activityXid)) {
throw new ApiError(400, 'Valid activityXid is required');
}
if (!receiverXid || isNaN(receiverXid)) {
throw new ApiError(400, 'Valid receiverXid is required');
}
const result = await chatService.sendMessage({
activityXid,
senderXid: hostUserId,
receiverXid,
message,
status,
});
return {
statusCode: 201,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Message sent successfully',
data: result,
}),
};
}
);

View File

@@ -0,0 +1,77 @@
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
import { ChatService } from '../../../../common/services/chat.service';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
const chatService = new ChatService(prismaClient);
export const handler = safeHandler(
async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
const token =
event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
400,
'This is a protected route. Please provide a valid token.'
);
}
const userInfo = await verifyUserToken(token);
const userId = Number(userInfo.id);
if (!userId || isNaN(userId)) {
throw new ApiError(400, 'Invalid user ID');
}
const activityXidParam =
event.queryStringParameters?.activityXid ||
event.queryStringParameters?.activity_xid;
const otherUserXidParam =
event.queryStringParameters?.otherUserXid ||
event.queryStringParameters?.other_user_xid ||
event.queryStringParameters?.userXid ||
event.queryStringParameters?.user_xid;
const limitParam = event.queryStringParameters?.limit;
const activityXid = Number(activityXidParam);
const otherUserXid = Number(otherUserXidParam);
const limit = limitParam ? Number(limitParam) : undefined;
if (!activityXid || isNaN(activityXid)) {
throw new ApiError(400, 'Valid activityXid is required');
}
if (!otherUserXid || isNaN(otherUserXid)) {
throw new ApiError(400, 'Valid otherUserXid is required');
}
const messages = await chatService.getMessages({
activityXid,
userXid: userId,
otherUserXid,
limit,
});
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Messages retrieved successfully',
data: messages,
}),
};
}
);

View File

@@ -0,0 +1,78 @@
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
import { ChatService } from '../../../../common/services/chat.service';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
const chatService = new ChatService(prismaClient);
export const handler = safeHandler(
async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
const token =
event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
400,
'This is a protected route. Please provide a valid token.'
);
}
const userInfo = await verifyUserToken(token);
const userId = Number(userInfo.id);
if (!userId || isNaN(userId)) {
throw new ApiError(400, 'Invalid user ID');
}
let body: any;
try {
body = JSON.parse(event.body || '{}');
} catch {
throw new ApiError(400, 'Invalid JSON in request body');
}
const activityXid = Number(body.activityXid ?? body.activity_xid);
const receiverXid = Number(
body.receiverXid ?? body.receivedXid ?? body.received_xid
);
const message = body.message;
const status = body.status;
if (!activityXid || isNaN(activityXid)) {
throw new ApiError(400, 'Valid activityXid is required');
}
if (!receiverXid || isNaN(receiverXid)) {
throw new ApiError(400, 'Valid receiverXid is required');
}
const result = await chatService.sendMessage({
activityXid,
senderXid: userId,
receiverXid,
message,
status,
});
return {
statusCode: 201,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Message sent successfully',
data: result,
}),
};
}
);