package service import ( "bytes" "context" "encoding/base64" "errors" "fmt" "github.com/aws/aws-sdk-go/aws/credentials" "image" "image/jpeg" "log" "strings" "tailly_back_v2/internal/domain" "tailly_back_v2/internal/repository" "tailly_back_v2/internal/utils" "tailly_back_v2/pkg/moderation" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3/s3manager" ) // Интерфейс сервиса постов type PostService interface { Create(ctx context.Context, authorID int, title, content string) (*domain.Post, error) GetByID(ctx context.Context, id int) (*domain.Post, error) GetAll(ctx context.Context) ([]*domain.Post, error) GetByAuthorID(ctx context.Context, authorID int) ([]*domain.Post, error) Update(ctx context.Context, id int, title, content string) (*domain.Post, error) Delete(ctx context.Context, id int) error } // Реализация сервиса постов type postService struct { postRepo repository.PostRepository } // Конструктор сервиса func NewPostService(postRepo repository.PostRepository) PostService { return &postService{postRepo: postRepo} } // Создание нового поста func (s *postService) Create(ctx context.Context, authorID int, title, content string) (*domain.Post, error) { const op = "service/postService.Create" // Валидация данных if title == "" { return nil, errors.New("post title cannot be empty") } if content == "" { return nil, errors.New("post content cannot be empty") } if strings.HasPrefix(content, "data:image/jpeg;base64,") { content = strings.TrimPrefix(content, "data:image/jpeg;base64,") } imgBase64, err := base64.StdEncoding.DecodeString(content) if err != nil { log.Println("failed to decode image base64", op, err) return nil, err } // Проверяем, что это изображение img, format, err := image.Decode(bytes.NewReader(imgBase64)) if err != nil { return nil, err } // Проверяем формат (например, только JPEG/PNG) if format != "jpeg" && format != "png" { return nil, errors.New("only JPEG/PNG allowed") } // Проверяем размер (например, не больше 10MB) if len(imgBase64) > 10_000_000 { return nil, errors.New("image too large (max 10MB)") } // Создаем клиент модерации modClient, err := moderation.NewModerationClient("localhost:50051") if err != nil { return nil, fmt.Errorf("failed to create moderation client: %v", err) } defer modClient.Close() // Проверяем изображение allowed, err := modClient.CheckImage(ctx, imgBase64) if err != nil { return nil, fmt.Errorf("image moderation failed: %v", err) } if !allowed { return nil, errors.New("image rejected by moderation service") } randName := utils.GenerateId() + ".jpg" buf := new(bytes.Buffer) if err := jpeg.Encode(buf, img, nil); err != nil { return nil, err } sess := session.Must(session.NewSession(&aws.Config{ Region: aws.String("ru-central1"), Endpoint: aws.String("https://s3.regru.cloud"), S3ForcePathStyle: aws.Bool(true), Credentials: credentials.NewStaticCredentials( "TJ946G2S1Z5FEI3I7DQQ", "C2H2aITHRDpek8H921yhnrINZwDoADsjW3F6HURl", "", ), })) uploader := s3manager.NewUploader(sess) s3Key := fmt.Sprintf("posts/%d/%s", authorID, randName) _, err = uploader.Upload(&s3manager.UploadInput{ Bucket: aws.String("tailly"), Key: aws.String(s3Key), Body: bytes.NewReader(buf.Bytes()), ContentType: aws.String("image/jpeg"), }) if err != nil { return nil, fmt.Errorf("failed to upload to S3: %v", err) } imageURL := fmt.Sprintf("https://s3.regru.cloud/%s/%s", "tailly", s3Key) post := &domain.Post{ Title: title, Content: imageURL, AuthorID: authorID, CreatedAt: time.Now(), UpdatedAt: time.Now(), } if err := s.postRepo.Create(ctx, post); err != nil { return nil, err } return post, nil } // Получение поста по ID func (s *postService) GetByID(ctx context.Context, id int) (*domain.Post, error) { post, err := s.postRepo.GetByID(ctx, id) if err != nil { if errors.Is(err, repository.ErrPostNotFound) { return nil, errors.New("post not found") } return nil, err } return post, nil } // Получение всех постов func (s *postService) GetAll(ctx context.Context) ([]*domain.Post, error) { posts, err := s.postRepo.GetAll(ctx) if err != nil { return nil, err } // Возвращаем пустой слайс вместо nil if posts == nil { return []*domain.Post{}, nil } return posts, nil } // Получение постов автора func (s *postService) GetByAuthorID(ctx context.Context, authorID int) ([]*domain.Post, error) { posts, err := s.postRepo.GetByAuthorID(ctx, authorID) if err != nil { return nil, err } if posts == nil { return []*domain.Post{}, nil } return posts, nil } // Обновление поста func (s *postService) Update(ctx context.Context, id int, title, content string) (*domain.Post, error) { // Получаем существующий пост post, err := s.postRepo.GetByID(ctx, id) if err != nil { return nil, err } // Обновляем поля post.Title = title post.Content = content post.UpdatedAt = time.Now() if err := s.postRepo.Update(ctx, post); err != nil { return nil, err } return post, nil } // Удаление поста func (s *postService) Delete(ctx context.Context, id int) error { return s.postRepo.Delete(ctx, id) }