package service import ( "context" "errors" "fmt" "io" "log" "math/rand" "mime" "narasi-ahli-be/app/database/entity" articleApprovalsRepository "narasi-ahli-be/app/module/article_approvals/repository" articleCategoriesRepository "narasi-ahli-be/app/module/article_categories/repository" articleCategoryDetailsRepository "narasi-ahli-be/app/module/article_category_details/repository" articleCategoryDetailsReq "narasi-ahli-be/app/module/article_category_details/request" articleFilesRepository "narasi-ahli-be/app/module/article_files/repository" "narasi-ahli-be/app/module/articles/mapper" "narasi-ahli-be/app/module/articles/repository" "narasi-ahli-be/app/module/articles/request" "narasi-ahli-be/app/module/articles/response" usersRepository "narasi-ahli-be/app/module/users/repository" config "narasi-ahli-be/config/config" minioStorage "narasi-ahli-be/config/config" "narasi-ahli-be/utils/paginator" utilSvc "narasi-ahli-be/utils/service" "path/filepath" "strconv" "strings" "time" "github.com/gofiber/fiber/v2" "github.com/minio/minio-go/v7" "github.com/rs/zerolog" ) // ArticlesService type articlesService struct { Repo repository.ArticlesRepository ArticleCategoriesRepo articleCategoriesRepository.ArticleCategoriesRepository ArticleFilesRepo articleFilesRepository.ArticleFilesRepository ArticleApprovalsRepo articleApprovalsRepository.ArticleApprovalsRepository ArticleCategoryDetailsRepo articleCategoryDetailsRepository.ArticleCategoryDetailsRepository Log zerolog.Logger Cfg *config.Config UsersRepo usersRepository.UsersRepository MinioStorage *minioStorage.MinioStorage } // ArticlesService define interface of IArticlesService type ArticlesService interface { All(req request.ArticlesQueryRequest) (articles []*response.ArticlesResponse, paging paginator.Pagination, err error) Show(id uint) (articles *response.ArticlesResponse, err error) ShowByOldId(oldId uint) (articles *response.ArticlesResponse, err error) Save(req request.ArticlesCreateRequest, authToken string) (articles *entity.Articles, err error) SaveThumbnail(c *fiber.Ctx) (err error) Update(id uint, req request.ArticlesUpdateRequest) (err error) Delete(id uint) error UpdateActivityCount(id uint, activityTypeId int) (err error) UpdateApproval(id uint, statusId int, userLevelId int, userLevelNumber int, userParentLevelId int) (err error) UpdateBanner(id uint, isBanner bool) error Viewer(c *fiber.Ctx) error SummaryStats(authToken string) (summaryStats *response.ArticleSummaryStats, err error) ArticlePerUserLevelStats(authToken string, startDate *string, endDate *string) (articlePerUserLevelStats []*response.ArticlePerUserLevelStats, err error) ArticleMonthlyStats(authToken string, year *int) (articleMonthlyStats []*response.ArticleMonthlyStats, err error) PublishScheduling(id uint, publishSchedule string) error } // NewArticlesService init ArticlesService func NewArticlesService( repo repository.ArticlesRepository, articleCategoriesRepo articleCategoriesRepository.ArticleCategoriesRepository, articleCategoryDetailsRepo articleCategoryDetailsRepository.ArticleCategoryDetailsRepository, articleFilesRepo articleFilesRepository.ArticleFilesRepository, articleApprovalsRepo articleApprovalsRepository.ArticleApprovalsRepository, log zerolog.Logger, cfg *config.Config, usersRepo usersRepository.UsersRepository, minioStorage *minioStorage.MinioStorage) ArticlesService { return &articlesService{ Repo: repo, ArticleCategoriesRepo: articleCategoriesRepo, ArticleCategoryDetailsRepo: articleCategoryDetailsRepo, ArticleFilesRepo: articleFilesRepo, ArticleApprovalsRepo: articleApprovalsRepo, Log: log, UsersRepo: usersRepo, MinioStorage: minioStorage, Cfg: cfg, } } // All implement interface of ArticlesService func (_i *articlesService) All(req request.ArticlesQueryRequest) (articless []*response.ArticlesResponse, paging paginator.Pagination, err error) { if req.Category != nil { findCategory, err := _i.ArticleCategoriesRepo.FindOneBySlug(*req.Category) if err != nil { return nil, paging, err } req.CategoryId = &findCategory.ID } results, paging, err := _i.Repo.GetAll(req) if err != nil { return } _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:articlesService", "Methods:All"). Interface("results", results).Msg("") host := _i.Cfg.App.Domain for _, result := range results { articleRes := mapper.ArticlesResponseMapper(_i.Log, host, result, _i.ArticleCategoriesRepo, _i.ArticleCategoryDetailsRepo, _i.ArticleFilesRepo, _i.UsersRepo) articless = append(articless, articleRes) } return } func (_i *articlesService) Show(id uint) (articles *response.ArticlesResponse, err error) { result, err := _i.Repo.FindOne(id) if err != nil { return nil, err } host := _i.Cfg.App.Domain return mapper.ArticlesResponseMapper(_i.Log, host, result, _i.ArticleCategoriesRepo, _i.ArticleCategoryDetailsRepo, _i.ArticleFilesRepo, _i.UsersRepo), nil } func (_i *articlesService) ShowByOldId(oldId uint) (articles *response.ArticlesResponse, err error) { result, err := _i.Repo.FindByOldId(oldId) if err != nil { return nil, err } host := _i.Cfg.App.Domain return mapper.ArticlesResponseMapper(_i.Log, host, result, _i.ArticleCategoriesRepo, _i.ArticleCategoryDetailsRepo, _i.ArticleFilesRepo, _i.UsersRepo), nil } func (_i *articlesService) Save(req request.ArticlesCreateRequest, authToken string) (articles *entity.Articles, err error) { _i.Log.Info().Interface("data", req).Msg("") newReq := req.ToEntity() var userLevelNumber int var userParentLevelId int if req.CreatedById != nil { createdBy, err := _i.UsersRepo.FindOne(*req.CreatedById) if err != nil { return nil, fmt.Errorf("User not found") } newReq.CreatedById = &createdBy.ID userLevelNumber = createdBy.UserLevel.LevelNumber if createdBy.UserLevel.ParentLevelId != nil { userParentLevelId = *createdBy.UserLevel.ParentLevelId } } else { createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) newReq.CreatedById = &createdBy.ID userLevelNumber = createdBy.UserLevel.LevelNumber if createdBy.UserLevel.ParentLevelId != nil { userParentLevelId = *createdBy.UserLevel.ParentLevelId } } isDraft := true if req.IsDraft == &isDraft { draftedAt := time.Now() newReq.IsDraft = &isDraft newReq.DraftedAt = &draftedAt isPublishFalse := false newReq.IsPublish = &isPublishFalse newReq.PublishedAt = nil } isPublish := true if req.IsPublish == &isPublish { publishedAt := time.Now() newReq.IsPublish = &isPublish newReq.PublishedAt = &publishedAt isDraftFalse := false newReq.IsDraft = &isDraftFalse newReq.DraftedAt = nil } if req.CreatedAt != nil { layout := "2006-01-02 15:04:05" parsedTime, err := time.Parse(layout, *req.CreatedAt) if err != nil { return nil, fmt.Errorf("Error parsing time:", err) } newReq.CreatedAt = parsedTime } // Approval statusIdOne := 1 statusIdTwo := 2 isPublishFalse := false createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if createdBy != nil && *createdBy.UserLevel.IsApprovalActive == false { newReq.NeedApprovalFrom = nil newReq.StatusId = &statusIdTwo } else { newReq.NeedApprovalFrom = &userParentLevelId newReq.StatusId = &statusIdOne newReq.IsPublish = &isPublishFalse newReq.PublishedAt = nil } saveArticleRes, err := _i.Repo.Create(newReq) if err != nil { return nil, err } // Approval var articleApproval *entity.ArticleApprovals if createdBy != nil && *createdBy.UserLevel.IsApprovalActive == true { articleApproval = &entity.ArticleApprovals{ ArticleId: saveArticleRes.ID, ApprovalBy: *newReq.CreatedById, StatusId: statusIdOne, Message: "Need Approval", ApprovalAtLevel: &userLevelNumber, } } else { articleApproval = &entity.ArticleApprovals{ ArticleId: saveArticleRes.ID, ApprovalBy: *newReq.CreatedById, StatusId: statusIdTwo, Message: "Publish Otomatis", ApprovalAtLevel: nil, } } _, err = _i.ArticleApprovalsRepo.Create(articleApproval) if err != nil { return nil, err } var categoryIds []string if req.CategoryIds != "" { categoryIds = strings.Split(req.CategoryIds, ",") } _i.Log.Info().Interface("categoryIds", categoryIds).Msg("") for _, categoryId := range categoryIds { categoryIdInt, _ := strconv.Atoi(categoryId) _i.Log.Info().Interface("categoryIdUint", uint(categoryIdInt)).Msg("") findCategory, err := _i.ArticleCategoriesRepo.FindOne(uint(categoryIdInt)) _i.Log.Info().Interface("findCategory", findCategory).Msg("") if err != nil { return nil, err } if findCategory == nil { return nil, errors.New("category not found") } categoryReq := articleCategoryDetailsReq.ArticleCategoryDetailsCreateRequest{ ArticleId: saveArticleRes.ID, CategoryId: categoryIdInt, IsActive: true, } newCategoryReq := categoryReq.ToEntity() err = _i.ArticleCategoryDetailsRepo.Create(newCategoryReq) if err != nil { return nil, err } } return saveArticleRes, nil } func (_i *articlesService) SaveThumbnail(c *fiber.Ctx) (err error) { id, err := strconv.ParseUint(c.Params("id"), 10, 0) if err != nil { return err } _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:articlesService", "Methods:SaveThumbnail"). Interface("id", id).Msg("") bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName form, err := c.MultipartForm() if err != nil { return err } files := form.File["files"] // Create minio connection. minioClient, err := _i.MinioStorage.ConnectMinio() if err != nil { // Return status 500 and minio connection error. return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": true, "msg": err.Error(), }) } // Iterasi semua file yang diunggah for _, file := range files { _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:Resource", "Uploader:: loop1"). Interface("data", file).Msg("") src, err := file.Open() if err != nil { return err } defer src.Close() filename := filepath.Base(file.Filename) filename = strings.ReplaceAll(filename, " ", "") filenameWithoutExt := filepath.Clean(filename[:len(filename)-len(filepath.Ext(filename))]) extension := filepath.Ext(file.Filename)[1:] now := time.Now() rand.New(rand.NewSource(now.UnixNano())) randUniqueId := rand.Intn(1000000) newFilenameWithoutExt := filenameWithoutExt + "_" + strconv.Itoa(randUniqueId) newFilename := newFilenameWithoutExt + "." + extension objectName := fmt.Sprintf("articles/thumbnail/%d/%d/%s", now.Year(), now.Month(), newFilename) findCategory, err := _i.Repo.FindOne(uint(id)) findCategory.ThumbnailName = &newFilename findCategory.ThumbnailPath = &objectName err = _i.Repo.Update(uint(id), findCategory) if err != nil { return err } // Upload file ke MinIO _, err = minioClient.PutObject(context.Background(), bucketName, objectName, src, file.Size, minio.PutObjectOptions{}) if err != nil { return err } } return } func (_i *articlesService) Update(id uint, req request.ArticlesUpdateRequest) (err error) { _i.Log.Info().Interface("data", req).Msg("") newReq := req.ToEntity() if req.CreatedAt != nil { layout := "2006-01-02 15:04:05" parsedTime, err := time.Parse(layout, *req.CreatedAt) if err != nil { return fmt.Errorf("Error parsing time:", err) } newReq.CreatedAt = parsedTime } return _i.Repo.UpdateSkipNull(id, newReq) } func (_i *articlesService) Delete(id uint) error { result, err := _i.Repo.FindOne(id) if err != nil { return err } isActive := false result.IsActive = &isActive return _i.Repo.Update(id, result) } func (_i *articlesService) Viewer(c *fiber.Ctx) (err error) { thumbnailName := c.Params("thumbnailName") emptyImage := "empty-image.jpg" searchThumbnail := emptyImage if thumbnailName != emptyImage { result, err := _i.Repo.FindByFilename(thumbnailName) if err != nil { return err } _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:Resource", "articlesService:Viewer"). Interface("resultThumbnail", result.ThumbnailPath).Msg("") if result.ThumbnailPath != nil { searchThumbnail = *result.ThumbnailPath } else { searchThumbnail = "articles/thumbnail/" + emptyImage } } else { searchThumbnail = "articles/thumbnail/" + emptyImage } _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:Resource", "articlesService:Viewer"). Interface("searchThumbnail", searchThumbnail).Msg("") ctx := context.Background() bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName objectName := searchThumbnail // Create minio connection. minioClient, err := _i.MinioStorage.ConnectMinio() if err != nil { // Return status 500 and minio connection error. return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": true, "msg": err.Error(), }) } fileContent, err := minioClient.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{}) if err != nil { log.Fatalln(err) } defer fileContent.Close() contentType := mime.TypeByExtension("." + getFileExtension(objectName)) if contentType == "" { contentType = "application/octet-stream" } c.Set("Content-Type", contentType) if _, err := io.Copy(c.Response().BodyWriter(), fileContent); err != nil { return err } return } func (_i *articlesService) UpdateActivityCount(id uint, activityTypeId int) error { result, err := _i.Repo.FindOne(id) if err != nil { return err } viewCount := 0 if result.ViewCount != nil { viewCount = *result.ViewCount } shareCount := 0 if result.ShareCount != nil { shareCount = *result.ShareCount } commentCount := 0 if result.CommentCount != nil { commentCount = *result.CommentCount } if activityTypeId == 2 { viewCount++ } else if activityTypeId == 3 { shareCount++ } else if activityTypeId == 4 { commentCount++ } result.ViewCount = &viewCount result.ShareCount = &shareCount result.CommentCount = &commentCount return _i.Repo.Update(id, result) } func (_i *articlesService) SummaryStats(authToken string) (summaryStats *response.ArticleSummaryStats, err error) { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) result, err := _i.Repo.SummaryStats(user.ID) if err != nil { return nil, err } return result, nil } func (_i *articlesService) ArticlePerUserLevelStats(authToken string, startDate *string, endDate *string) (articlePerUserLevelStats []*response.ArticlePerUserLevelStats, err error) { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:Resource", "articlesService:ArticlePerUserLevelStats"). Interface("startDate", startDate).Msg("") _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:Resource", "articlesService:ArticlePerUserLevelStats"). Interface("endDate", endDate).Msg("") var userLevelId *uint var userLevelNumber *int if user != nil { userLevelId = &user.UserLevelId userLevelNumber = &user.UserLevel.LevelNumber } _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:Resource", "articlesService:ArticlePerUserLevelStats"). Interface("userLevelId", userLevelId).Msg("") _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:Resource", "articlesService:ArticlePerUserLevelStats"). Interface("userLevelNumber", userLevelNumber).Msg("") result, err := _i.Repo.ArticlePerUserLevelStats(userLevelId, userLevelNumber, nil, nil) if err != nil { return nil, err } return result, nil } func (_i *articlesService) ArticleMonthlyStats(authToken string, year *int) (articleMonthlyStats []*response.ArticleMonthlyStats, err error) { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) var userLevelId *uint var userLevelNumber *int if user != nil { userLevelId = &user.UserLevelId userLevelNumber = &user.UserLevel.LevelNumber } result, err := _i.Repo.ArticleMonthlyStats(userLevelId, userLevelNumber, *year) if err != nil { return nil, err } return result, nil } func (_i *articlesService) UpdateApproval(id uint, statusId int, userLevelId int, userLevelNumber int, userParentLevelId int) (err error) { result, err := _i.Repo.FindOne(id) if err != nil { return err } _i.Log.Info().Interface("statusId", statusId).Msg("") statusIdOne := 1 statusIdTwo := 2 statusIdThree := 3 isPublish := true isDraftFalse := false if statusId == 2 { if userLevelNumber == 2 || userLevelNumber == 3 { result.NeedApprovalFrom = &userParentLevelId result.StatusId = &statusIdOne } else { result.NeedApprovalFrom = nil result.StatusId = &statusIdTwo result.IsPublish = &isPublish publishedAt := time.Now() result.PublishedAt = &publishedAt result.IsDraft = &isDraftFalse result.DraftedAt = nil } userLevelIdStr := strconv.Itoa(userLevelId) if result.HasApprovedBy == nil { result.HasApprovedBy = &userLevelIdStr } else { hasApprovedBySlice := strings.Split(*result.HasApprovedBy, ",") hasApprovedBySlice = append(hasApprovedBySlice, userLevelIdStr) hasApprovedByJoin := strings.Join(hasApprovedBySlice, ",") result.HasApprovedBy = &hasApprovedByJoin } } else if statusId == 3 { result.StatusId = &statusIdThree result.NeedApprovalFrom = nil result.HasApprovedBy = nil } err = _i.Repo.Update(id, result) if err != nil { return err } return } func (_i *articlesService) PublishScheduling(id uint, publishSchedule string) error { result, err := _i.Repo.FindOne(id) if err != nil { return err } result.PublishSchedule = &publishSchedule return _i.Repo.Update(id, result) } func (_i *articlesService) UpdateBanner(id uint, isBanner bool) error { result, err := _i.Repo.FindOne(id) if err != nil { return err } result.IsBanner = &isBanner return _i.Repo.Update(id, result) } func getFileExtension(filename string) string { // split file name parts := strings.Split(filename, ".") // jika tidak ada ekstensi, kembalikan string kosong if len(parts) == 1 || (len(parts) == 2 && parts[0] == "") { return "" } // ambil ekstensi terakhir return parts[len(parts)-1] }