package service import ( "context" "errors" "fmt" "github.com/gofiber/fiber/v2" "github.com/minio/minio-go/v7" "github.com/rs/zerolog" "go-humas-be/app/database/entity" articleApprovalsRepository "go-humas-be/app/module/article_approvals/repository" articleCategoriesRepository "go-humas-be/app/module/article_categories/repository" articleCategoryDetailsRepository "go-humas-be/app/module/article_category_details/repository" articleCategoryDetailsReq "go-humas-be/app/module/article_category_details/request" articleFilesRepository "go-humas-be/app/module/article_files/repository" "go-humas-be/app/module/articles/mapper" "go-humas-be/app/module/articles/repository" "go-humas-be/app/module/articles/request" "go-humas-be/app/module/articles/response" usersRepository "go-humas-be/app/module/users/repository" config "go-humas-be/config/config" minioStorage "go-humas-be/config/config" "go-humas-be/utils/paginator" utilSvc "go-humas-be/utils/service" "io" "log" "math/rand" "mime" "path/filepath" "strconv" "strings" "time" ) // 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) 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) 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 ExecuteScheduling() 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) 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 userParentLevelId = *createdBy.UserLevel.ParentLevelId } else { createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) newReq.CreatedById = &createdBy.ID userLevelNumber = createdBy.UserLevel.LevelNumber 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:] rand.New(rand.NewSource(time.Now().UnixNano())) randUniqueId := rand.Intn(1000000) newFilenameWithoutExt := filenameWithoutExt + "_" + strconv.Itoa(randUniqueId) newFilename := newFilenameWithoutExt + "." + extension objectName := "articles/thumbnail/" + 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) ExecuteScheduling() error { articles, err := _i.Repo.GetAllPublishSchedule() if err != nil { return err } layout := "2006-01-02" now := time.Now() today := now.Truncate(24 * time.Hour) for _, article := range articles { // Looping setiap artikel if article.PublishSchedule == nil { continue } parsedDate, err := time.Parse(layout, *article.PublishSchedule) if err != nil { continue } if parsedDate.Equal(today) { isPublish := true statusIdTwo := 2 article.PublishSchedule = nil article.IsPublish = &isPublish article.PublishedAt = &now article.StatusId = &statusIdTwo if err := _i.Repo.Update(article.ID, article); err != nil { _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:articlesService", "Methods:ExecuteScheduling"). Interface("Failed to publish Article ID : ", article.ID).Msg("") } } } return err } 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] }