155 أسطر
5.5 KiB
TypeScript
155 أسطر
5.5 KiB
TypeScript
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
|
import { Types } from 'mongoose';
|
|
import { FeedVersionService } from '../../infrastructure/cache/feed-version.service';
|
|
import { NotificationsService } from '../notifications/notifications.service';
|
|
import { CommentsRepository } from '../comments/comments.repository';
|
|
import { PostsRepository } from '../posts/posts.repository';
|
|
import { LikesRepository } from './likes.repository';
|
|
import { ToggleLikeDto } from './dto/toggle-like.dto';
|
|
|
|
@Injectable()
|
|
export class LikesService {
|
|
private readonly logger = new Logger(LikesService.name);
|
|
|
|
constructor(
|
|
private readonly likesRepository: LikesRepository,
|
|
private readonly postsRepository: PostsRepository,
|
|
private readonly commentsRepository: CommentsRepository,
|
|
private readonly feedVersionService: FeedVersionService,
|
|
private readonly notificationsService: NotificationsService,
|
|
) {}
|
|
|
|
async toggle(userId: string, dto: ToggleLikeDto): Promise<{ liked: boolean; targetId: string; targetType: string }> {
|
|
const existing = await this.likesRepository.findOne(userId, dto.targetId, dto.targetType);
|
|
return existing ? this.unlike(userId, dto) : this.like(userId, dto);
|
|
}
|
|
|
|
async like(userId: string, dto: ToggleLikeDto): Promise<{ liked: boolean; targetId: string; targetType: string }> {
|
|
await this.assertTargetExists(dto);
|
|
const notificationContext = await this.resolveNotificationContext(dto);
|
|
|
|
const existing = await this.likesRepository.findOne(userId, dto.targetId, dto.targetType);
|
|
if (existing) {
|
|
return { liked: true, targetId: dto.targetId, targetType: dto.targetType };
|
|
}
|
|
|
|
await this.likesRepository.create(userId, dto.targetId, dto.targetType);
|
|
if (dto.targetType === 'post') {
|
|
await this.postsRepository.incrementLikesCount(dto.targetId, 1);
|
|
}
|
|
await this.feedVersionService.bumpGlobalVersion();
|
|
if (notificationContext.recipientId && notificationContext.recipientId !== userId) {
|
|
try {
|
|
await this.notificationsService.createLikeNotification(
|
|
userId,
|
|
notificationContext.recipientId,
|
|
dto.targetId,
|
|
{
|
|
resourceType: dto.targetType,
|
|
previewText: notificationContext.previewText,
|
|
},
|
|
);
|
|
} catch (error) {
|
|
this.logger.warn(
|
|
`Like notification failed for actor=${userId} recipient=${notificationContext.recipientId}: ${
|
|
error instanceof Error ? error.message : 'unknown error'
|
|
}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
return { liked: true, targetId: dto.targetId, targetType: dto.targetType };
|
|
}
|
|
|
|
async unlike(userId: string, dto: ToggleLikeDto): Promise<{ liked: boolean; targetId: string; targetType: string }> {
|
|
await this.assertTargetExists(dto);
|
|
|
|
const existing = await this.likesRepository.findOne(userId, dto.targetId, dto.targetType);
|
|
if (!existing) {
|
|
return { liked: false, targetId: dto.targetId, targetType: dto.targetType };
|
|
}
|
|
|
|
await this.likesRepository.deleteById(existing.id);
|
|
if (dto.targetType === 'post') {
|
|
await this.postsRepository.incrementLikesCount(dto.targetId, -1);
|
|
}
|
|
await this.feedVersionService.bumpGlobalVersion();
|
|
|
|
return { liked: false, targetId: dto.targetId, targetType: dto.targetType };
|
|
}
|
|
|
|
async getStatus(userId: string, dto: ToggleLikeDto): Promise<{ liked: boolean; targetId: string; targetType: string }> {
|
|
const targetExists = await this.targetExists(dto);
|
|
if (!targetExists) {
|
|
return { liked: false, targetId: dto.targetId, targetType: dto.targetType };
|
|
}
|
|
|
|
const existing = await this.likesRepository.findOne(userId, dto.targetId, dto.targetType);
|
|
return { liked: !!existing, targetId: dto.targetId, targetType: dto.targetType };
|
|
}
|
|
|
|
private async assertTargetExists(dto: ToggleLikeDto): Promise<void> {
|
|
const targetExists = await this.targetExists(dto);
|
|
if (!targetExists) {
|
|
throw new NotFoundException(dto.targetType === 'post' ? 'Post not found' : 'Comment not found');
|
|
}
|
|
}
|
|
|
|
private async targetExists(dto: ToggleLikeDto): Promise<boolean> {
|
|
if (dto.targetType === 'post') {
|
|
const post = await this.postsRepository.findById(dto.targetId);
|
|
return !!post;
|
|
}
|
|
|
|
const comment = await this.commentsRepository.findById(dto.targetId);
|
|
return !!comment;
|
|
}
|
|
|
|
private async resolveNotificationContext(
|
|
dto: ToggleLikeDto,
|
|
): Promise<{ recipientId: string; previewText: string }> {
|
|
if (dto.targetType === 'post') {
|
|
const post = await this.postsRepository.findById(dto.targetId);
|
|
return {
|
|
recipientId: this.extractEntityId(post?.authorId),
|
|
previewText: (post?.content ?? '').slice(0, 140),
|
|
};
|
|
}
|
|
|
|
const comment = await this.commentsRepository.findById(dto.targetId);
|
|
return {
|
|
recipientId: comment?.authorId?.toString?.() ?? '',
|
|
previewText: (comment?.content ?? '').slice(0, 140),
|
|
};
|
|
}
|
|
|
|
private extractEntityId(value: unknown): string {
|
|
if (!value) {
|
|
return '';
|
|
}
|
|
|
|
if (typeof value === 'string') {
|
|
return value;
|
|
}
|
|
|
|
if (value instanceof Types.ObjectId) {
|
|
return value.toString();
|
|
}
|
|
|
|
if (typeof value === 'object') {
|
|
const candidate = value as { _id?: unknown; id?: unknown };
|
|
if (candidate._id instanceof Types.ObjectId) {
|
|
return candidate._id.toString();
|
|
}
|
|
if (typeof candidate._id === 'string') {
|
|
return candidate._id;
|
|
}
|
|
if (typeof candidate.id === 'string') {
|
|
return candidate.id;
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
}
|