interaction system
This commit is contained in:
7
src/ws/dto/interaction-payload.dto.ts
Normal file
7
src/ws/dto/interaction-payload.dto.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export class InteractionPayloadDto {
|
||||
senderUserId: string;
|
||||
senderName: string;
|
||||
content: string;
|
||||
type: string;
|
||||
timestamp: string;
|
||||
}
|
||||
16
src/ws/dto/send-interaction.dto.ts
Normal file
16
src/ws/dto/send-interaction.dto.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { IsNotEmpty, IsString, IsUUID, MaxLength } from 'class-validator';
|
||||
|
||||
export class SendInteractionDto {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
recipientUserId: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(500)
|
||||
content: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
type: string;
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import { JwtVerificationService } from '../../auth/services/jwt-verification.ser
|
||||
import { PrismaService } from '../../database/prisma.service';
|
||||
import { UserSocketService } from './user-socket.service';
|
||||
import { WsNotificationService } from './ws-notification.service';
|
||||
import { SendInteractionDto } from '../dto/send-interaction.dto';
|
||||
import { WsException } from '@nestjs/websockets';
|
||||
|
||||
interface MockSocket extends Partial<AuthenticatedSocket> {
|
||||
id: string;
|
||||
@@ -472,4 +474,130 @@ describe('StateGateway', () => {
|
||||
).rejects.toThrow('Unauthorized');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleSendInteraction', () => {
|
||||
it('should send interaction to friend if online', async () => {
|
||||
const mockClient: MockSocket = {
|
||||
id: 'client1',
|
||||
data: {
|
||||
user: { keycloakSub: 'test-sub', name: 'TestUser' },
|
||||
userId: 'user-1',
|
||||
friends: new Set(['friend-1']),
|
||||
},
|
||||
emit: jest.fn(),
|
||||
};
|
||||
|
||||
const data: SendInteractionDto = {
|
||||
recipientUserId: 'friend-1',
|
||||
content: 'hello',
|
||||
type: 'text',
|
||||
};
|
||||
|
||||
(mockUserSocketService.isUserOnline as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
await gateway.handleSendInteraction(
|
||||
mockClient as unknown as AuthenticatedSocket,
|
||||
data,
|
||||
);
|
||||
|
||||
expect(mockWsNotificationService.emitToUser).toHaveBeenCalledWith(
|
||||
'friend-1',
|
||||
'interaction-received',
|
||||
expect.objectContaining({
|
||||
senderUserId: 'user-1',
|
||||
senderName: 'TestUser',
|
||||
content: 'hello',
|
||||
type: 'text',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail if recipient is not a friend', async () => {
|
||||
const mockClient: MockSocket = {
|
||||
id: 'client1',
|
||||
data: {
|
||||
user: { keycloakSub: 'test-sub' },
|
||||
userId: 'user-1',
|
||||
friends: new Set(['friend-1']),
|
||||
},
|
||||
emit: jest.fn(),
|
||||
};
|
||||
|
||||
const data: SendInteractionDto = {
|
||||
recipientUserId: 'stranger-1',
|
||||
content: 'hello',
|
||||
type: 'text',
|
||||
};
|
||||
|
||||
await gateway.handleSendInteraction(
|
||||
mockClient as unknown as AuthenticatedSocket,
|
||||
data,
|
||||
);
|
||||
|
||||
expect(mockClient.emit).toHaveBeenCalledWith(
|
||||
'interaction-delivery-failed',
|
||||
expect.objectContaining({
|
||||
reason: 'Recipient is not a friend',
|
||||
}),
|
||||
);
|
||||
expect(mockWsNotificationService.emitToUser).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fail if recipient is offline', async () => {
|
||||
const mockClient: MockSocket = {
|
||||
id: 'client1',
|
||||
data: {
|
||||
user: { keycloakSub: 'test-sub' },
|
||||
userId: 'user-1',
|
||||
friends: new Set(['friend-1']),
|
||||
},
|
||||
emit: jest.fn(),
|
||||
};
|
||||
|
||||
const data: SendInteractionDto = {
|
||||
recipientUserId: 'friend-1',
|
||||
content: 'hello',
|
||||
type: 'text',
|
||||
};
|
||||
|
||||
(mockUserSocketService.isUserOnline as jest.Mock).mockResolvedValue(
|
||||
false,
|
||||
);
|
||||
|
||||
await gateway.handleSendInteraction(
|
||||
mockClient as unknown as AuthenticatedSocket,
|
||||
data,
|
||||
);
|
||||
|
||||
expect(mockClient.emit).toHaveBeenCalledWith(
|
||||
'interaction-delivery-failed',
|
||||
expect.objectContaining({
|
||||
reason: 'Recipient is offline',
|
||||
}),
|
||||
);
|
||||
expect(mockWsNotificationService.emitToUser).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw Unauthorized if user not initialized', async () => {
|
||||
const mockClient: MockSocket = {
|
||||
id: 'client1',
|
||||
data: {
|
||||
// Missing user/userId
|
||||
},
|
||||
};
|
||||
|
||||
const data: SendInteractionDto = {
|
||||
recipientUserId: 'friend-1',
|
||||
content: 'hello',
|
||||
type: 'text',
|
||||
};
|
||||
|
||||
await expect(
|
||||
gateway.handleSendInteraction(
|
||||
mockClient as unknown as AuthenticatedSocket,
|
||||
data,
|
||||
),
|
||||
).rejects.toThrow(WsException);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,8 @@ import type { AuthenticatedSocket } from '../../types/socket';
|
||||
import { AuthService } from '../../auth/auth.service';
|
||||
import { JwtVerificationService } from '../../auth/services/jwt-verification.service';
|
||||
import { CursorPositionDto } from '../dto/cursor-position.dto';
|
||||
import { SendInteractionDto } from '../dto/send-interaction.dto';
|
||||
import { InteractionPayloadDto } from '../dto/interaction-payload.dto';
|
||||
import { PrismaService } from '../../database/prisma.service';
|
||||
import { UserSocketService } from './user-socket.service';
|
||||
import { WsNotificationService } from './ws-notification.service';
|
||||
@@ -336,4 +338,55 @@ export class StateGateway
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeMessage(WS_EVENT.CLIENT_SEND_INTERACTION)
|
||||
async handleSendInteraction(
|
||||
client: AuthenticatedSocket,
|
||||
data: SendInteractionDto,
|
||||
) {
|
||||
const user = client.data.user;
|
||||
const currentUserId = client.data.userId;
|
||||
|
||||
if (!user || !currentUserId) {
|
||||
throw new WsException('Unauthorized: User not initialized');
|
||||
}
|
||||
|
||||
// 1. Verify recipient is a friend
|
||||
const friends = client.data.friends;
|
||||
if (!friends || !friends.has(data.recipientUserId)) {
|
||||
client.emit(WS_EVENT.INTERACTION_DELIVERY_FAILED, {
|
||||
recipientUserId: data.recipientUserId,
|
||||
reason: 'Recipient is not a friend',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Check if recipient is online
|
||||
const isOnline = await this.userSocketService.isUserOnline(
|
||||
data.recipientUserId,
|
||||
);
|
||||
if (!isOnline) {
|
||||
client.emit(WS_EVENT.INTERACTION_DELIVERY_FAILED, {
|
||||
recipientUserId: data.recipientUserId,
|
||||
reason: 'Recipient is offline',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Construct payload
|
||||
const payload: InteractionPayloadDto = {
|
||||
senderUserId: currentUserId,
|
||||
senderName: user.name || user.username || 'Unknown',
|
||||
content: data.content,
|
||||
type: data.type,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// 4. Send to recipient
|
||||
await this.wsNotificationService.emitToUser(
|
||||
data.recipientUserId,
|
||||
WS_EVENT.INTERACTION_RECEIVED,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ export const WS_EVENT = {
|
||||
FRIEND_DOLL_UPDATED: 'friend-doll-updated',
|
||||
FRIEND_DOLL_DELETED: 'friend-doll-deleted',
|
||||
FRIEND_ACTIVE_DOLL_CHANGED: 'friend-active-doll-changed',
|
||||
CLIENT_SEND_INTERACTION: 'client-send-interaction',
|
||||
INTERACTION_RECEIVED: 'interaction-received',
|
||||
INTERACTION_DELIVERY_FAILED: 'interaction-delivery-failed',
|
||||
} as const;
|
||||
|
||||
export const REDIS_CHANNEL = {
|
||||
|
||||
Reference in New Issue
Block a user