diff --git a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index c000833..40361d4 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -26,7 +26,9 @@ public class HandlerConfig { ReferralHandler referralHandler, ResetPasswordHandler resetPasswordHandler, DaDataHandler daDataHandler, - PaymentHandler paymentHandler + PaymentHandler paymentHandler, + GetViolationsHandler getViolationsHandler, + MonitoringHandler monitoringHandler ) { Map map = new HashMap<>(); map.put(20001, login); @@ -44,6 +46,8 @@ public class HandlerConfig { map.put(30003, referralHandler); map.put(30004, daDataHandler); map.put(30005, paymentHandler); + map.put(30006, getViolationsHandler); + map.put(30007, monitoringHandler); return map; } diff --git a/src/main/java/ru/soune/nocopy/dto/file/YandexSearchResponse.java b/src/main/java/ru/soune/nocopy/dto/file/YandexSearchResponse.java index f064ad3..3542068 100644 --- a/src/main/java/ru/soune/nocopy/dto/file/YandexSearchResponse.java +++ b/src/main/java/ru/soune/nocopy/dto/file/YandexSearchResponse.java @@ -12,11 +12,6 @@ public class YandexSearchResponse { @JsonProperty("images") private List images; -// @JsonProperty("images") -// public void setImages(List images) { -// this.images = images; -// } - @Data @JsonIgnoreProperties(ignoreUnknown = true) public static class ImageResult { diff --git a/src/main/java/ru/soune/nocopy/dto/monitoring/GetViolationsRequest.java b/src/main/java/ru/soune/nocopy/dto/monitoring/GetViolationsRequest.java new file mode 100644 index 0000000..9c9dda1 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/monitoring/GetViolationsRequest.java @@ -0,0 +1,23 @@ +package ru.soune.nocopy.dto.monitoring; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class GetViolationsRequest { + + @JsonProperty("file_id") + private String fileId; + + @JsonProperty("page") + private Integer page; + + @JsonProperty("page_size") + private Integer pageSize; + + @JsonProperty("mark_as_read") + private boolean markAsRead; + + @JsonProperty("auth_token") + private String authToken; +} diff --git a/src/main/java/ru/soune/nocopy/dto/monitoring/MonitoringStatusResponse.java b/src/main/java/ru/soune/nocopy/dto/monitoring/MonitoringStatusResponse.java new file mode 100644 index 0000000..b9b832a --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/monitoring/MonitoringStatusResponse.java @@ -0,0 +1,40 @@ +package ru.soune.nocopy.dto.monitoring; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import ru.soune.nocopy.entity.monitoring.MonitoringType; + +import java.time.LocalDateTime; + +/** + * Response for status monitoring + */ +@Data +@Builder +public class MonitoringStatusResponse { + + @JsonProperty("file_id") + private String fileId; + + @JsonProperty("file_name") + private String fileName; + + @JsonProperty("monitoring_type") + private MonitoringType monitoringType; + + @JsonProperty("next_run") + private LocalDateTime nextRun; + + @JsonProperty("last_run") + private LocalDateTime lastRun; + + @JsonProperty("last_run_status") + private String lastRunStatus; + + @JsonProperty("is_active") + private boolean isActive; + + @JsonProperty("new_violations_count") + private long newViolationsCount; +} diff --git a/src/main/java/ru/soune/nocopy/dto/monitoring/SetMonitoringRequest.java b/src/main/java/ru/soune/nocopy/dto/monitoring/SetMonitoringRequest.java new file mode 100644 index 0000000..4694f65 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/monitoring/SetMonitoringRequest.java @@ -0,0 +1,20 @@ +package ru.soune.nocopy.dto.monitoring; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import ru.soune.nocopy.entity.monitoring.MonitoringType; +/** + Set monitoring request + */ +@Data +public class SetMonitoringRequest { + + @JsonProperty("file_id") + private String fileId; + + @JsonProperty("monitoring_type") + private MonitoringType monitoringType; + + @JsonProperty("auth_token") + private String authToken; +} diff --git a/src/main/java/ru/soune/nocopy/dto/monitoring/ViolationResponse.java b/src/main/java/ru/soune/nocopy/dto/monitoring/ViolationResponse.java new file mode 100644 index 0000000..188b4b8 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/monitoring/ViolationResponse.java @@ -0,0 +1,43 @@ +package ru.soune.nocopy.dto.monitoring; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import ru.soune.nocopy.entity.monitoring.SearchEngine; + +import java.time.LocalDateTime; + +@Data +@Builder +public class ViolationResponse { + + @JsonProperty("id") + private String id; + + @JsonProperty("file_id") + private String fileId; + + @JsonProperty("file_name") + private String fileName; + + @JsonProperty("found_url") + private String foundUrl; + + @JsonProperty("page_url") + private String pageUrl; + + @JsonProperty("page_title") + private String pageTitle; + + @JsonProperty("host") + private String host; + + @JsonProperty("search_engine") + private SearchEngine searchEngine; + + @JsonProperty("found_at") + private LocalDateTime foundAt; + + @JsonProperty("is_new") + private boolean isNew; +} diff --git a/src/main/java/ru/soune/nocopy/entity/monitoring/FileMonitoringEntity.java b/src/main/java/ru/soune/nocopy/entity/monitoring/FileMonitoringEntity.java new file mode 100644 index 0000000..87fea5c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/monitoring/FileMonitoringEntity.java @@ -0,0 +1,60 @@ +package ru.soune.nocopy.entity.monitoring; + +import jakarta.persistence.*; +import lombok.*; +import ru.soune.nocopy.entity.file.FileEntity; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "file_monitoring") +public class FileMonitoringEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private String id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "file_id", nullable = false) + private FileEntity file; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Enumerated(EnumType.STRING) + @Column(name = "monitoring_type", nullable = false) + private MonitoringType monitoringType; + + @Column(name = "next_run", nullable = false) + private LocalDateTime nextRun; + + @Column(name = "last_run") + private LocalDateTime lastRun; + + @Column(name = "last_run_status") + private String lastRunStatus; + + @Column(name = "is_active", nullable = false) + private boolean isActive = true; + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @PrePersist + public void prePersist() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + @PreUpdate + public void preUpdate() { + this.updatedAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/entity/monitoring/FoundViolationEntity.java b/src/main/java/ru/soune/nocopy/entity/monitoring/FoundViolationEntity.java new file mode 100644 index 0000000..d91ed8f --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/monitoring/FoundViolationEntity.java @@ -0,0 +1,76 @@ +package ru.soune.nocopy.entity.monitoring; + +import jakarta.persistence.*; +import lombok.*; +import ru.soune.nocopy.entity.file.FileEntity; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "found_violations", + indexes = { + @Index(name = "idx_file_id", columnList = "file_id"), + @Index(name = "idx_found_at", columnList = "found_at") + }) +public class FoundViolationEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private String id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "file_id", nullable = false) + private FileEntity file; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(name = "file_id_copy", nullable = false) + private String fileId; + + @Column(name = "file_name_copy") + private String fileName; + + @Column(name = "found_url", nullable = false, length = 2048) + private String foundUrl; + + @Column(name = "page_url", length = 2048) + private String pageUrl; + + @Column(name = "page_title") + private String pageTitle; + + @Column(name = "host") + private String host; + + @Column(name = "image_width") + private Integer imageWidth; + + @Column(name = "image_height") + private Integer imageHeight; + + @Enumerated(EnumType.STRING) + @Column(name = "search_engine", nullable = false) + private SearchEngine searchEngine; + + @Column(name = "search_session_id") + private String searchSessionId; + + @Column(name = "found_at", nullable = false) + private LocalDateTime foundAt; + + @Column(name = "is_new", nullable = false) + private boolean isNew = true; + + @Column(name = "is_notified", nullable = false) + private boolean isNotified = false; + + @PrePersist + public void prePersist() { + this.foundAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/ru/soune/nocopy/entity/monitoring/MonitoringType.java b/src/main/java/ru/soune/nocopy/entity/monitoring/MonitoringType.java new file mode 100644 index 0000000..21a8b84 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/monitoring/MonitoringType.java @@ -0,0 +1,6 @@ +package ru.soune.nocopy.entity.monitoring; + + +public enum MonitoringType { + MONITORING_DAILY, MONITORING_WEEKLY, MONITORING_MONTHLY, NONE +} diff --git a/src/main/java/ru/soune/nocopy/entity/monitoring/SearchEngine.java b/src/main/java/ru/soune/nocopy/entity/monitoring/SearchEngine.java new file mode 100644 index 0000000..97a663c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/monitoring/SearchEngine.java @@ -0,0 +1,8 @@ +package ru.soune.nocopy.entity.monitoring; + + +public enum SearchEngine { + YANDEX, + GOOGLE, + BOTH +} diff --git a/src/main/java/ru/soune/nocopy/entity/tarif/TariffType.java b/src/main/java/ru/soune/nocopy/entity/tarif/TariffType.java index 6507b51..aa3eb2b 100644 --- a/src/main/java/ru/soune/nocopy/entity/tarif/TariffType.java +++ b/src/main/java/ru/soune/nocopy/entity/tarif/TariffType.java @@ -2,5 +2,6 @@ package ru.soune.nocopy.entity.tarif; public enum TariffType { - START, BASIC, PRO, ENTERPRISE, DEMO, BUISNES, CORPORAT, STUDIO, FREE, TOKEN_100, TOKEN_500, TOKEN_1000, TOKEN_5000; + START, BASIC, PRO, ENTERPRISE, DEMO, BUISNES, CORPORAT, STUDIO, FREE, TOKEN_100, TOKEN_500, TOKEN_1000, TOKEN_5000, + MONITORING_DAILY, MONITORING_WEEKLY, MONITORING_MONTHLY; } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/exception/UserNotHavePermission.java b/src/main/java/ru/soune/nocopy/exception/UserNotHavePermission.java new file mode 100644 index 0000000..7704209 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/UserNotHavePermission.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class UserNotHavePermission extends RuntimeException { + public UserNotHavePermission(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/GetViolationsHandler.java b/src/main/java/ru/soune/nocopy/handler/GetViolationsHandler.java new file mode 100644 index 0000000..edd261e --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/GetViolationsHandler.java @@ -0,0 +1,85 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.MessageCode; +import ru.soune.nocopy.dto.monitoring.GetViolationsRequest; +import ru.soune.nocopy.dto.monitoring.ViolationResponse; +import ru.soune.nocopy.entity.monitoring.FoundViolationEntity; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.repository.FoundViolationRepository; +import ru.soune.nocopy.service.register.AuthService; + +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Component +@RequiredArgsConstructor +public class GetViolationsHandler implements RequestHandler { + private final ObjectMapper objectMapper; + + private final FoundViolationRepository violationRepository; + + private final AuthService authService; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + GetViolationsRequest violationsRequest = objectMapper.convertValue( + request.getMessageBody(), GetViolationsRequest.class); + + String authToken = violationsRequest.getAuthToken(); + + //TODO add return for null token + if (authToken == null) { + return new BaseResponse(0, 0, "", new Object()); + } + + User user = authService.getAuthToken(authToken).getUser(); + List violations; + + if (violationsRequest.getFileId() != null) { + violations = violationRepository.findByFileIdOrderByFoundAtDesc( + violationsRequest.getFileId()); + } else { + violations = violationRepository.findByUserIdOrderByFoundAtDesc(user.getId()); + } + + if (violationsRequest.isMarkAsRead()) { + violations.stream() + .filter(FoundViolationEntity::isNew) + .forEach(v -> v.setNew(false)); + violationRepository.saveAll(violations); + } + + List response = violations.stream() + .map(this::mapToResponse) + .collect(Collectors.toList()); + + return new BaseResponse( + request.getMsgId(), + MessageCode.SUCCESS.getCode(), + "Violations retrieved", + response + ); + } + + private ViolationResponse mapToResponse(FoundViolationEntity entity) { + return ViolationResponse.builder() + .id(entity.getId()) + .fileId(entity.getFileId()) + .fileName(entity.getFileName()) + .foundUrl(entity.getFoundUrl()) + .pageUrl(entity.getPageUrl()) + .pageTitle(entity.getPageTitle()) + .host(entity.getHost()) + .searchEngine(entity.getSearchEngine()) + .foundAt(entity.getFoundAt()) + .isNew(entity.isNew()) + .build(); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/MonitoringHandler.java b/src/main/java/ru/soune/nocopy/handler/MonitoringHandler.java new file mode 100644 index 0000000..8a19270 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/MonitoringHandler.java @@ -0,0 +1,72 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.MessageCode; +import ru.soune.nocopy.dto.monitoring.MonitoringStatusResponse; +import ru.soune.nocopy.dto.monitoring.SetMonitoringRequest; +import ru.soune.nocopy.entity.monitoring.MonitoringType; +import ru.soune.nocopy.entity.tarif.TariffType; +import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.repository.TariffRepository; +import ru.soune.nocopy.service.monitoring.FileMonitoringService; +import ru.soune.nocopy.service.register.AuthService; +import ru.soune.nocopy.service.tariff.TariffService; + +import java.util.Map; + +@Slf4j +@Component +@RequiredArgsConstructor +public class MonitoringHandler implements RequestHandler { + + private final ObjectMapper objectMapper; + + private final FileMonitoringService monitoringService; + + private final AuthService authService; + + private final TariffService tariffService; + + private final TariffRepository tariffRepository; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + SetMonitoringRequest setRequest = objectMapper.convertValue( + request.getMessageBody(), SetMonitoringRequest.class); + + String authToken = setRequest.getAuthToken(); + AuthToken userAuthToken = authService.getAuthToken(authToken); + + if (authToken == null || userAuthToken == null) { + return new BaseResponse(30007, MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(), + MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(), + Map.of("token", authToken)); + } + + User user = userAuthToken.getUser(); + Long userId = user.getId(); + MonitoringType monitoringType = setRequest.getMonitoringType(); + + monitoringService.setMonitoring(userId, setRequest.getFileId(), monitoringType); + + MonitoringStatusResponse status = monitoringService.getMonitoringStatus( + setRequest.getFileId(), userId); + + return new BaseResponse( + request.getMsgId(), + MessageCode.SUCCESS.getCode(), + "Monitoring settings updated", + status); + } + //TODO check tokens for apply + private boolean checkTokensForMonitoring(User user, MonitoringType monitoringType) { + tariffService.getTariffByType(TariffType.valueOf(monitoringType.name())); + return true; + } +} diff --git a/src/main/java/ru/soune/nocopy/repository/FileMonitoringRepository.java b/src/main/java/ru/soune/nocopy/repository/FileMonitoringRepository.java new file mode 100644 index 0000000..126aa7a --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/FileMonitoringRepository.java @@ -0,0 +1,25 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity; +import ru.soune.nocopy.entity.monitoring.MonitoringType; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Repository +public interface FileMonitoringRepository extends JpaRepository { + + Optional findByFileId(String fileId); + + List findByUserIdAndIsActiveTrue(Long userId); + + @Query("SELECT fm FROM FileMonitoringEntity fm WHERE fm.isActive = true AND fm.monitoringType != 'NONE' AND fm.nextRun <= :now") + List findReadyForRun(@Param("now") LocalDateTime now); + + List findByMonitoringTypeAndIsActiveTrue(MonitoringType type); +} diff --git a/src/main/java/ru/soune/nocopy/repository/FoundViolationRepository.java b/src/main/java/ru/soune/nocopy/repository/FoundViolationRepository.java new file mode 100644 index 0000000..c6ed9a5 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/FoundViolationRepository.java @@ -0,0 +1,30 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.monitoring.FoundViolationEntity; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +public interface FoundViolationRepository extends JpaRepository { + + List findByFileIdOrderByFoundAtDesc(String fileId); + + List findByUserIdOrderByFoundAtDesc(Long userId); + + @Query("SELECT fv FROM FoundViolationEntity fv WHERE fv.userId = :userId AND fv.isNew = true") + List findNewViolationsByUser(@Param("userId") Long userId); + + @Query("SELECT fv FROM FoundViolationEntity fv WHERE fv.file.id = :fileId AND fv.foundUrl = :foundUrl") + List findDuplicate(@Param("fileId") String fileId, @Param("foundUrl") String foundUrl); + + List findByFoundAtBetween(LocalDateTime start, LocalDateTime end); + + @Query("SELECT COUNT(fv) FROM FoundViolationEntity fv WHERE fv.userId = :userId AND fv.isNew = true") + long countNewByUserId(@Param("userId") Long userId); +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/scheduler/MonitoringScheduler.java b/src/main/java/ru/soune/nocopy/scheduler/MonitoringScheduler.java new file mode 100644 index 0000000..442bcbe --- /dev/null +++ b/src/main/java/ru/soune/nocopy/scheduler/MonitoringScheduler.java @@ -0,0 +1,47 @@ +package ru.soune.nocopy.scheduler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity; +import ru.soune.nocopy.repository.FileMonitoringRepository; +import ru.soune.nocopy.service.monitoring.MonitoringSearchService; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class MonitoringScheduler { + + private final FileMonitoringRepository monitoringRepository; + + private final MonitoringSearchService monitoringSearchService; + + @Scheduled(cron = "0 1 2 * * *", zone = "Europe/Moscow") + @Transactional + public void processScheduledMonitoring() { + log.info("Starting scheduled monitoring check at 02:01 MSK"); + + LocalDateTime now = LocalDateTime.now(); + List readyForRun = monitoringRepository.findReadyForRun(now); + + log.info("Found {} files ready for monitoring", readyForRun.size()); + + //TODO check tokens for apply + for (FileMonitoringEntity monitoring : readyForRun) { + try { + monitoringSearchService.processFileSearch(monitoring); + log.info("Successfully queued search for file: {}", monitoring.getFile().getId()); + } catch (Exception e) { + log.error("Failed to process monitoring for file: {}", monitoring.getFile().getId(), e); + monitoring.setLastRunStatus("ERROR: " + e.getMessage()); + monitoringRepository.save(monitoring); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java b/src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java new file mode 100644 index 0000000..85b6e44 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java @@ -0,0 +1,101 @@ +package ru.soune.nocopy.service.monitoring; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.monitoring.MonitoringStatusResponse; +import ru.soune.nocopy.entity.file.*; +import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity; +import ru.soune.nocopy.entity.monitoring.MonitoringType; +import ru.soune.nocopy.exception.NotValidFieldException; +import ru.soune.nocopy.exception.UserNotHavePermission; +import ru.soune.nocopy.repository.FileEntityRepository; +import ru.soune.nocopy.repository.FileMonitoringRepository; +import ru.soune.nocopy.repository.FoundViolationRepository; + +import java.io.FileNotFoundException; +import java.time.LocalDateTime; +import java.time.temporal.TemporalAdjusters; + +@Slf4j +@Service +@RequiredArgsConstructor +public class FileMonitoringService { + + private final FileMonitoringRepository monitoringRepository; + private final FileEntityRepository fileRepository; + private final FoundViolationRepository violationRepository; + + @Transactional + public FileMonitoringEntity setMonitoring(Long userId, String fileId, MonitoringType type) throws FileNotFoundException { + FileEntity file = fileRepository.findById(fileId) + .orElseThrow(() -> new FileNotFoundException(fileId)); + + if (!file.getUserId().equals(userId)) { + throw new UserNotHavePermission("User not Have permission for file"); + } + + FileMonitoringEntity monitoring = monitoringRepository.findByFileId(fileId) + .orElse(FileMonitoringEntity.builder() + .file(file) + .userId(userId) + .build()); + + monitoring.setMonitoringType(type); + monitoring.setActive(type != MonitoringType.NONE); + monitoring.setNextRun(calculateNextRun(type, LocalDateTime.now())); + monitoring.setUpdatedAt(LocalDateTime.now()); + + if (monitoring.getCreatedAt() == null) { + monitoring.setCreatedAt(LocalDateTime.now()); + } + + return monitoringRepository.save(monitoring); + } + + @Transactional + public void updateNextRun(FileMonitoringEntity monitoring) { + if (monitoring.isActive() && monitoring.getMonitoringType() != MonitoringType.NONE) { + LocalDateTime nextRun = calculateNextRun(monitoring.getMonitoringType(), LocalDateTime.now()); + monitoring.setNextRun(nextRun); + monitoringRepository.save(monitoring); + } + } + + + private LocalDateTime calculateNextRun(MonitoringType type, LocalDateTime from) { + LocalDateTime nextRun = from.withHour(2).withMinute(0).withSecond(0).withNano(0); + + if (from.isAfter(nextRun)) { + nextRun = nextRun.plusDays(1); + } + + return switch (type) { + case MONITORING_DAILY -> nextRun; + case MONITORING_WEEKLY -> nextRun.with(TemporalAdjusters.nextOrSame(java.time.DayOfWeek.MONDAY)); + case MONITORING_MONTHLY -> nextRun.with(TemporalAdjusters.firstDayOfNextMonth()); + case NONE -> null; + }; + } + + public MonitoringStatusResponse getMonitoringStatus(String fileId, Long userId) { + FileMonitoringEntity monitoring = monitoringRepository.findByFileId(fileId).orElse(null); + FileEntity file = fileRepository.findById(fileId).orElseThrow(); + + long newViolations = violationRepository.countNewByUserId(userId); + + return MonitoringStatusResponse.builder() + .fileId(fileId) + .fileName(file.getOriginalFileName()) + .monitoringType(monitoring != null ? monitoring.getMonitoringType() : MonitoringType.NONE) + .nextRun(monitoring != null ? monitoring.getNextRun() : null) + .lastRun(monitoring != null ? monitoring.getLastRun() : null) + .lastRunStatus(monitoring != null ? monitoring.getLastRunStatus() : null) + .isActive(monitoring != null && monitoring.isActive()) + .newViolationsCount(newViolations) + .build(); + } + +} diff --git a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java new file mode 100644 index 0000000..3068959 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java @@ -0,0 +1,130 @@ +package ru.soune.nocopy.service.monitoring; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.dto.file.YandexSearchResponse; +import ru.soune.nocopy.entity.file.*; +import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity; +import ru.soune.nocopy.entity.monitoring.FoundViolationEntity; +import ru.soune.nocopy.entity.monitoring.MonitoringType; +import ru.soune.nocopy.entity.monitoring.SearchEngine; +import ru.soune.nocopy.repository.FileMonitoringRepository; +import ru.soune.nocopy.repository.FoundViolationRepository; +import ru.soune.nocopy.service.search.SearchImageService; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeoutException; + +@Slf4j +@Service +@RequiredArgsConstructor +public class MonitoringSearchService { + + private final SearchImageService searchImageService; + private final FoundViolationRepository violationRepository; + private final FileMonitoringRepository monitoringRepository; + + @Transactional + public void processFileSearch(FileMonitoringEntity monitoring) { + String searchSessionId = UUID.randomUUID().toString(); + log.info("Starting search session: {} for file: {}", searchSessionId, monitoring.getFile().getId()); + + monitoring.setLastRun(LocalDateTime.now()); + monitoring.setLastRunStatus("IN_PROGRESS"); + monitoringRepository.save(monitoring); + + try { + String yandexResponse = searchImageService.searchReverseByPublicUrl( + monitoring.getFile(), "yandex_reverse_image", "visual_matches"); + + List yandexImages = + searchImageService.getAllImagesWithoutPagination(yandexResponse, "visual_matches"); + + int savedCount = saveFoundImages(monitoring, yandexImages, SearchEngine.YANDEX, searchSessionId); + + try { + String googleResponse = searchImageService.searchReverseByPublicUrl( + monitoring.getFile(), "google_lens", "exact_matches"); + + List googleImages = + searchImageService.getAllImagesWithoutPagination(googleResponse, "exact_matches"); + + savedCount += saveFoundImages(monitoring, googleImages, SearchEngine.GOOGLE, searchSessionId); + + } catch (TimeoutException | IOException e) { + log.warn("Google search failed for session: {}", searchSessionId, e); + } + + monitoring.setLastRunStatus("SUCCESS - Found " + savedCount + " violations"); + + } catch (Exception e) { + log.error("Search failed for session: {}", searchSessionId, e); + monitoring.setLastRunStatus("ERROR: " + e.getMessage()); + } finally { + updateNextRun(monitoring); + monitoringRepository.save(monitoring); + } + } + + private int saveFoundImages(FileMonitoringEntity monitoring, + List images, + SearchEngine engine, + String sessionId) { + int savedCount = 0; + + for (YandexSearchResponse.ImageResult image : images) { + List existing = violationRepository.findDuplicate( + monitoring.getFile().getId(), image.getUrl()); + + if (existing.isEmpty()) { + FoundViolationEntity violation = FoundViolationEntity.builder() + .file(monitoring.getFile()) + .fileId(monitoring.getFile().getId()) + .fileName(monitoring.getFile().getOriginalFileName()) + .userId(monitoring.getUserId()) + .foundUrl(image.getUrl()) + .pageUrl(image.getPageUrl()) + .pageTitle(image.getPageTitle()) + .host(image.getHost()) + .imageWidth(image.getWidth()) + .imageHeight(image.getHeight()) + .searchEngine(engine) + .searchSessionId(sessionId) + .isNew(true) + .isNotified(false) + .build(); + + violationRepository.save(violation); + savedCount++; + } + } + + return savedCount; + } + + private void updateNextRun(FileMonitoringEntity monitoring) { + LocalDateTime nextRun = calculateNextRun(monitoring.getMonitoringType()); + monitoring.setNextRun(nextRun); + } + + private LocalDateTime calculateNextRun(MonitoringType type) { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime nextRun = now.withHour(2).withMinute(0).withSecond(0).withNano(0); + + if (now.isAfter(nextRun)) { + nextRun = nextRun.plusDays(1); + } + + return switch (type) { + case MONITORING_DAILY -> nextRun; + case MONITORING_WEEKLY -> nextRun.plusWeeks(1); + case MONITORING_MONTHLY -> nextRun.plusMonths(1); + default -> null; + }; + } +} diff --git a/src/main/java/ru/soune/nocopy/service/register/AuthService.java b/src/main/java/ru/soune/nocopy/service/register/AuthService.java index a8dc32c..652d45b 100644 --- a/src/main/java/ru/soune/nocopy/service/register/AuthService.java +++ b/src/main/java/ru/soune/nocopy/service/register/AuthService.java @@ -160,6 +160,10 @@ public class AuthService { return authTokenRepository.save(genereateAuthToken(user)); } + public AuthToken getAuthToken(String token) { + return authTokenRepository.findByToken(token).get(); + } + private AuthToken genereateAuthToken(User user) { AuthToken authToken = new AuthToken(); authToken.setToken(generateAuthToken()); diff --git a/src/main/java/ru/soune/nocopy/service/tariff/TariffInitializationService.java b/src/main/java/ru/soune/nocopy/service/tariff/TariffInitializationService.java index 1f5464c..72244d5 100644 --- a/src/main/java/ru/soune/nocopy/service/tariff/TariffInitializationService.java +++ b/src/main/java/ru/soune/nocopy/service/tariff/TariffInitializationService.java @@ -220,6 +220,37 @@ public class TariffInitializationService { tariff.setMaxUsers(0L); tariff.setTariffAccountType("token"); break; + + + case MONITORING_DAILY: + tariff.setName("monitoring_daily"); + tariff.setPrice(0); + tariff.setTokens(10); + tariff.setMaxFilesCount(0); + tariff.setDiskSize(0L); + tariff.setMaxUsers(0L); + tariff.setTariffAccountType("monitoring"); + break; + + case MONITORING_WEEKLY: + tariff.setName("monitoring_weekly"); + tariff.setPrice(0); + tariff.setTokens(25); + tariff.setMaxFilesCount(0); + tariff.setDiskSize(0L); + tariff.setMaxUsers(0L); + tariff.setTariffAccountType("monitoring"); + break; + + case MONITORING_MONTHLY: + tariff.setName("monitoring_monthly"); + tariff.setPrice(0); + tariff.setTokens(50); + tariff.setMaxFilesCount(0); + tariff.setDiskSize(0L); + tariff.setMaxUsers(0L); + tariff.setTariffAccountType("monitoring"); + break; } tariffRepository.save(tariff);