هذا الالتزام موجود في:
2026-04-20 15:12:16 +03:00
التزام 28f7241bcd
172 ملفات معدلة مع 21907 إضافات و0 حذوفات

عرض الملف

@@ -0,0 +1,155 @@
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
import { Types } from 'mongoose';
import { PostType } from '../../common/enums/post-type.enum';
import { PostVisibility } from '../../common/enums/post-visibility.enum';
import { UsersRepository } from '../users/users.repository';
import { CreatePostDto } from './dto/create-post.dto';
import { PostQueryDto } from './dto/post-query.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { PostDocument } from './schemas/post.schema';
import { PostsRepository } from './posts.repository';
@Injectable()
export class PostsService {
constructor(
private readonly postsRepository: PostsRepository,
private readonly usersRepository: UsersRepository,
) {}
async create(userId: string, dto: CreatePostDto): Promise<PostDocument> {
const postType = this.resolvePostType(dto.videoUrl, dto.audioUrl);
const hashtags = this.extractHashtags(dto.content);
const post = await this.postsRepository.create(userId, {
content: dto.content,
videoUrl: dto.videoUrl ?? '',
audioUrl: dto.audioUrl ?? '',
postType,
visibility: dto.visibility ?? PostVisibility.PUBLIC,
hashtags,
});
await this.usersRepository.incrementPostsCount(userId, 1);
return post;
}
async update(userId: string, postId: string, dto: UpdatePostDto): Promise<PostDocument> {
const post = await this.postsRepository.findById(postId);
if (!post) {
throw new NotFoundException('Post not found');
}
if (post.authorId.toString() !== userId) {
throw new ForbiddenException('You can only update your own posts');
}
const hasVideoUpdate = typeof dto.videoUrl !== 'undefined';
const hasAudioUpdate = typeof dto.audioUrl !== 'undefined';
const nextVideoUrl = hasVideoUpdate ? dto.videoUrl ?? '' : post.videoUrl ?? '';
const nextAudioUrl = hasAudioUpdate ? dto.audioUrl ?? '' : post.audioUrl ?? '';
const nextPostType = this.resolvePostType(nextVideoUrl, nextAudioUrl);
const payload: UpdatePostDto & { postType: PostType } = {
...dto,
postType: nextPostType,
};
if (typeof dto.content === 'string') {
(payload as UpdatePostDto & { postType: PostType; hashtags: string[] }).hashtags =
this.extractHashtags(dto.content);
}
if (hasVideoUpdate && !hasAudioUpdate) {
payload.audioUrl = '';
}
if (hasAudioUpdate && !hasVideoUpdate) {
payload.videoUrl = '';
}
const updated = await this.postsRepository.updateById(postId, payload);
if (!updated) {
throw new NotFoundException('Post not found');
}
return updated;
}
async remove(userId: string, postId: string): Promise<void> {
const post = await this.postsRepository.findById(postId);
if (!post) {
throw new NotFoundException('Post not found');
}
if (post.authorId.toString() !== userId) {
throw new ForbiddenException('You can only delete your own posts');
}
await this.postsRepository.deleteById(postId, userId);
await this.usersRepository.incrementPostsCount(userId, -1);
}
async findById(postId: string): Promise<PostDocument> {
const post = await this.postsRepository.findById(postId);
if (!post) {
throw new NotFoundException('Post not found');
}
return post;
}
async findUserPosts(userId: string, query: PostQueryDto) {
if (!Types.ObjectId.isValid(userId)) {
throw new BadRequestException('Invalid user id');
}
const page = query.page ?? 1;
const limit = query.limit ?? 20;
const skip = (page - 1) * limit;
const filter: Record<string, unknown> = { authorId: new Types.ObjectId(userId) };
if (query.visibility) {
filter.visibility = query.visibility;
}
const [items, total] = await Promise.all([
this.postsRepository.findMany(filter, skip, limit),
this.postsRepository.count(filter),
]);
return {
items,
page,
limit,
total,
totalPages: Math.ceil(total / limit) || 1,
};
}
private resolvePostType(videoUrl?: string, audioUrl?: string): PostType {
const hasVideo = !!videoUrl?.trim();
const hasAudio = !!audioUrl?.trim();
if (hasVideo && hasAudio) {
throw new BadRequestException('Post can contain either video or audio, not both');
}
if (hasVideo) {
return PostType.VIDEO;
}
if (hasAudio) {
return PostType.AUDIO;
}
return PostType.TEXT;
}
private extractHashtags(content: string): string[] {
const matches = content.match(/#[\p{L}\p{N}_]+/gu) ?? [];
const normalized = matches
.map((item) => item.replace('#', '').trim().toLowerCase())
.filter(Boolean);
return Array.from(new Set(normalized)).slice(0, 20);
}
}