From 760589a0ee3f74cf83e11a7fba81c3dcf915bf74 Mon Sep 17 00:00:00 2001 From: Jose Conde Date: Fri, 24 Oct 2025 16:56:04 +0200 Subject: [PATCH] Update --- Dockerfile | 33 +++++ build.gradle.kts | 4 + .../xintanalabs/rssotto/RssottoApplication.kt | 2 + .../components/checker/CheckerFactory.kt | 5 +- .../components/checker/IVersionChecker.kt | 4 +- .../components/checker/api/ApiChecker.kt | 2 +- .../checker/exceptions/CheckerException.kt | 7 + .../exceptions/ScraperFetcherException.kt | 7 + .../checker/manual/ManualChecker.kt | 17 +++ .../checker/scrape/FlareSolverrFetcher.kt | 72 ++++++++++ .../components/checker/scrape/JSoupFetcher.kt | 4 + .../checker/scrape/ScrapeChecker.kt | 98 ++++++++++---- .../rssotto/config/RssottoProperties.kt | 8 ++ .../rssotto/config/SecurityConfiguration.kt | 21 ++- .../rssotto/constants/Constants.kt | 3 + .../controller/actions/ActionsController.kt | 25 ++++ .../rssotto/controller/app/AppController.kt | 26 ++-- .../rssotto/controller/app/AppRequest.kt | 7 +- .../rssotto/controller/app/AppResponse.kt | 7 +- .../checkerType/CheckerTypeController.kt | 54 ++++++++ .../checkerType/CheckerTypeRequest.kt | 8 ++ .../checkerType/CheckerTypeResponse.kt | 9 ++ .../developer/DeveloperController.kt | 59 ++++++++ .../controller/developer/DeveloperRequest.kt | 7 + .../controller/developer/DeveloperResponse.kt | 8 ++ .../controller/source/SourceController.kt | 2 +- .../rssotto/controller/user/UserController.kt | 2 +- .../rssotto/db/mongodb/MongoDBAbstract.kt | 17 ++- .../rssotto/db/mongodb/MongoDBClient.kt | 5 + .../net/xintanalabs/rssotto/enums/ModType.kt | 5 + .../rssotto/enums/NotificationType.kt | 9 ++ .../net/xintanalabs/rssotto/enums/Role.kt | 5 + .../net/xintanalabs/rssotto/enums/Severity.kt | 5 + .../net/xintanalabs/rssotto/model/App.kt | 6 +- .../xintanalabs/rssotto/model/CheckerType.kt | 11 ++ .../xintanalabs/rssotto/model/Developer.kt | 10 ++ .../xintanalabs/rssotto/model/Notification.kt | 17 +++ .../xintanalabs/rssotto/model/SiteCache.kt | 11 ++ .../net/xintanalabs/rssotto/model/User.kt | 8 +- .../rssotto/repository/AppRepository.kt | 12 +- .../repository/CheckerTypeRepository.kt | 32 +++++ .../rssotto/repository/DeveloperRepository.kt | 27 ++++ .../repository/NotificationRepository.kt | 31 +++++ .../rssotto/repository/SiteCacheRepository.kt | 33 +++++ .../rssotto/repository/SourceRepository.kt | 7 +- .../rssotto/repository/TypeRepository.kt | 12 +- .../rssotto/repository/UserRepository.kt | 2 +- .../rssotto/service/ActionsService.kt | 127 ++++++++++++++++++ .../xintanalabs/rssotto/service/AppService.kt | 20 ++- .../rssotto/service/CheckerTypeService.kt | 22 +++ .../rssotto/service/DeveloperService.kt | 22 +++ .../rssotto/service/NotificationService.kt | 17 +++ .../rssotto/service/SiteCacheService.kt | 29 ++++ .../rssotto/service/TypeService.kt | 6 +- .../rssotto/tasks/ScheduledTasks.kt | 16 ++- src/main/resources/application.yaml | 6 +- .../rssotto/RssottoApplicationTests.kt | 5 +- 57 files changed, 939 insertions(+), 97 deletions(-) create mode 100644 Dockerfile create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/components/checker/exceptions/CheckerException.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/components/checker/exceptions/ScraperFetcherException.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/components/checker/manual/ManualChecker.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/components/checker/scrape/FlareSolverrFetcher.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/config/RssottoProperties.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/controller/actions/ActionsController.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/controller/checkerType/CheckerTypeController.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/controller/checkerType/CheckerTypeRequest.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/controller/checkerType/CheckerTypeResponse.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/controller/developer/DeveloperController.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/controller/developer/DeveloperRequest.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/controller/developer/DeveloperResponse.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/enums/ModType.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/enums/NotificationType.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/enums/Role.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/enums/Severity.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/model/CheckerType.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/model/Developer.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/model/Notification.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/model/SiteCache.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/repository/CheckerTypeRepository.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/repository/DeveloperRepository.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/repository/NotificationRepository.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/repository/SiteCacheRepository.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/service/ActionsService.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/service/CheckerTypeService.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/service/DeveloperService.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/service/NotificationService.kt create mode 100644 src/main/kotlin/net/xintanalabs/rssotto/service/SiteCacheService.kt diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c39c968 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# Use a JDK base image for building +FROM gradle:8.10.2-jdk17-alpine AS builder + +# Set working directory +WORKDIR /app + +# Copy build files +COPY build.gradle.kts settings.gradle.kts ./ +COPY src ./src + +# Build the application +RUN gradle build #--no-daemon + +# Use a lightweight JRE image for running +FROM eclipse-temurin:17-alpine + +# Set working directory +WORKDIR /app + +# Install Chromium and ChromeDriver +RUN apk add --no-cache chromium chromium-chromedriver nss + +ENV CHROME_BIN=/usr/bin/chromium-browser +ENV CHROMEDRIVER_BIN=/usr/bin/chromedriver + +# Copy the built JAR from the builder stage +COPY --from=builder /app/build/libs/*.jar app.jar + +# Expose the port (default for Spring Boot) +EXPOSE 8080 + +# Run the application +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index b0f20e6..5e9efda 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -43,6 +43,10 @@ dependencies { implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.seleniumhq.selenium:selenium-java:4.35.0") + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.google.code.gson:gson:2.10.1") + implementation("org.jsoup:jsoup:1.17.2") + testImplementation("org.springframework.security:spring-security-test") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") diff --git a/src/main/kotlin/net/xintanalabs/rssotto/RssottoApplication.kt b/src/main/kotlin/net/xintanalabs/rssotto/RssottoApplication.kt index 22b6847..77eeaf9 100644 --- a/src/main/kotlin/net/xintanalabs/rssotto/RssottoApplication.kt +++ b/src/main/kotlin/net/xintanalabs/rssotto/RssottoApplication.kt @@ -2,8 +2,10 @@ package net.xintanalabs.rssotto import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication +import org.springframework.scheduling.annotation.EnableScheduling @SpringBootApplication +@EnableScheduling class RssottoApplication fun main(args: Array) { diff --git a/src/main/kotlin/net/xintanalabs/rssotto/components/checker/CheckerFactory.kt b/src/main/kotlin/net/xintanalabs/rssotto/components/checker/CheckerFactory.kt index 923796d..15fc35c 100644 --- a/src/main/kotlin/net/xintanalabs/rssotto/components/checker/CheckerFactory.kt +++ b/src/main/kotlin/net/xintanalabs/rssotto/components/checker/CheckerFactory.kt @@ -1,19 +1,22 @@ package net.xintanalabs.rssotto.components.checkers import net.xintanalabs.rssotto.components.checker.api.ApiChecker +import net.xintanalabs.rssotto.components.checker.manual.ManualChecker import net.xintanalabs.rssotto.components.checkers.scrape.ScrapeChecker import org.springframework.stereotype.Component @Component class CheckerFactory( private val apiChecker: ApiChecker, - private val scrapeChecker: ScrapeChecker + private val scrapeChecker: ScrapeChecker, + private val manualChecker: ManualChecker ) { fun createChecker(type: String): IVersionChecker { val parts = type.split(":") return when (parts[0].lowercase()) { "scrape" -> scrapeChecker "api" -> apiChecker + "manual" -> manualChecker else -> throw IllegalArgumentException("Unknown checker type: $type") } } diff --git a/src/main/kotlin/net/xintanalabs/rssotto/components/checker/IVersionChecker.kt b/src/main/kotlin/net/xintanalabs/rssotto/components/checker/IVersionChecker.kt index 4f688a5..924297b 100644 --- a/src/main/kotlin/net/xintanalabs/rssotto/components/checker/IVersionChecker.kt +++ b/src/main/kotlin/net/xintanalabs/rssotto/components/checker/IVersionChecker.kt @@ -1,7 +1,5 @@ package net.xintanalabs.rssotto.components.checkers -import net.xintanalabs.rssotto.model.Source - interface IVersionChecker { - suspend fun getLatestVersion(paramsDict: Map): String + suspend fun getLatestVersion(paramsDict: Map): String? } \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/components/checker/api/ApiChecker.kt b/src/main/kotlin/net/xintanalabs/rssotto/components/checker/api/ApiChecker.kt index 8c0ce54..7b2332d 100644 --- a/src/main/kotlin/net/xintanalabs/rssotto/components/checker/api/ApiChecker.kt +++ b/src/main/kotlin/net/xintanalabs/rssotto/components/checker/api/ApiChecker.kt @@ -10,7 +10,7 @@ import kotlin.text.get @Component class ApiChecker(private val webClient: WebClient) : IVersionChecker { - override suspend fun getLatestVersion(paramsDict: Map): String { + override suspend fun getLatestVersion(paramsDict: Map): String? { val url = paramsDict["url"]?.takeIf { it.isNotEmpty() } ?: throw IllegalArgumentException("API URL required") val jsonPath = paramsDict["jsonPath"]?.takeIf { it.isNotEmpty() } diff --git a/src/main/kotlin/net/xintanalabs/rssotto/components/checker/exceptions/CheckerException.kt b/src/main/kotlin/net/xintanalabs/rssotto/components/checker/exceptions/CheckerException.kt new file mode 100644 index 0000000..d44a974 --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/components/checker/exceptions/CheckerException.kt @@ -0,0 +1,7 @@ +package net.xintanalabs.rssotto.components.checker.exceptions + +class CheckerException( + message: String?, + val info: Map? = null, + cause: Throwable? = null, +) : Exception(message, cause) \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/components/checker/exceptions/ScraperFetcherException.kt b/src/main/kotlin/net/xintanalabs/rssotto/components/checker/exceptions/ScraperFetcherException.kt new file mode 100644 index 0000000..e8abe44 --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/components/checker/exceptions/ScraperFetcherException.kt @@ -0,0 +1,7 @@ +package net.xintanalabs.rssotto.components.checker.exceptions + +class ScraperFetcherException( + message: String, + status: Int? = null, + cause: Throwable? = null +) : Exception(message, cause) \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/components/checker/manual/ManualChecker.kt b/src/main/kotlin/net/xintanalabs/rssotto/components/checker/manual/ManualChecker.kt new file mode 100644 index 0000000..8f05ec6 --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/components/checker/manual/ManualChecker.kt @@ -0,0 +1,17 @@ +package net.xintanalabs.rssotto.components.checker.manual + +import net.xintanalabs.rssotto.components.checkers.IVersionChecker +import net.xintanalabs.rssotto.service.AppService +import org.springframework.stereotype.Component + +@Component +class ManualChecker( + private val appService: AppService +): IVersionChecker { + override suspend fun getLatestVersion(paramsDict: Map): String? { + val id = paramsDict["id"] ?: return "" + + val app = appService.getAppById(id) + return app?.latestVersion + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/components/checker/scrape/FlareSolverrFetcher.kt b/src/main/kotlin/net/xintanalabs/rssotto/components/checker/scrape/FlareSolverrFetcher.kt new file mode 100644 index 0000000..bc4eca9 --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/components/checker/scrape/FlareSolverrFetcher.kt @@ -0,0 +1,72 @@ +package net.xintanalabs.rssotto.components.checker.scrape + +import com.google.gson.Gson +import com.google.gson.JsonObject +import net.xintanalabs.rssotto.components.checkers.scrape.IScrapeFetcher +import org.springframework.stereotype.Component +import okhttp3.* +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.IOException + +@Component(value="flaresolverr") +class FlaresSolverrFetcher: IScrapeFetcher { + + companion object { + private val client = OkHttpClient.Builder() + .connectTimeout(60, java.util.concurrent.TimeUnit.SECONDS) + .readTimeout(120, java.util.concurrent.TimeUnit.SECONDS) + .writeTimeout(120, java.util.concurrent.TimeUnit.SECONDS) + .build() + } + + override suspend fun fetch(url: String): String { + val flaresolverUrl = "http://192.168.1.112:8191/v1" + val gson = Gson() + + val createSessionData = mapOf("cmd" to "sessions.create") + val createJson = gson.toJson(createSessionData) + val createBody = createJson.toRequestBody("application/json".toMediaType()) + val createRequest = Request.Builder() + .url(flaresolverUrl) + .post(createBody) + .build() + + try { + val createResponse = client.newCall(createRequest).execute() + if (!createResponse.isSuccessful) { + throw IOException("Unexpected code ${createResponse.code}") + } + val jsonResponse = gson.fromJson(createResponse.body?.string(), JsonObject::class.java) + val sessionId = jsonResponse.get("session")?.asString + ?: throw IOException("No session ID received") + + val scrapeData = mapOf( + "cmd" to "request.get", + "url" to url, + "maxTimeout" to 60000, + "session" to sessionId + ) + val scrapeJson = gson.toJson(scrapeData) + val scrapeBody = scrapeJson.toRequestBody("application/json".toMediaType()) + val scrapeRequest = Request.Builder() + .url(flaresolverUrl) + .post(scrapeBody) + .build() + + val scrapeResponse = client.newCall(scrapeRequest).execute() + if (!scrapeResponse.isSuccessful) { + throw IOException("Failed to scrape: ${scrapeResponse.code}") + } + val scrapeJsonResponse = gson.fromJson(scrapeResponse.body?.string(), JsonObject::class.java) + val html = scrapeJsonResponse.getAsJsonObject("solution")?.get("response")?.asString + ?: throw IOException("No HTML received") + + return html + } catch (e: IOException) { + throw RuntimeException("Error during scraping: ${e.message}", e) + } finally { + client.dispatcher.executorService.shutdown() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/components/checker/scrape/JSoupFetcher.kt b/src/main/kotlin/net/xintanalabs/rssotto/components/checker/scrape/JSoupFetcher.kt index c4731e2..b80e195 100644 --- a/src/main/kotlin/net/xintanalabs/rssotto/components/checker/scrape/JSoupFetcher.kt +++ b/src/main/kotlin/net/xintanalabs/rssotto/components/checker/scrape/JSoupFetcher.kt @@ -1,6 +1,8 @@ package net.xintanalabs.rssotto.components.checkers.scrape import kotlinx.coroutines.delay +import net.xintanalabs.rssotto.components.checker.exceptions.ScraperFetcherException +import org.jsoup.HttpStatusException import org.jsoup.Jsoup import org.springframework.stereotype.Component @@ -18,6 +20,8 @@ class JSoupFetcher: IScrapeFetcher { .header("Upgrade-Insecure-Requests", "1") .get() .html() + } catch (httpe: HttpStatusException) { + throw ScraperFetcherException("HTTP error fetching ${url}: ${httpe.statusCode}", httpe.statusCode, httpe) } catch (e: Exception) { throw RuntimeException("Error fetching ${url}", e) } diff --git a/src/main/kotlin/net/xintanalabs/rssotto/components/checker/scrape/ScrapeChecker.kt b/src/main/kotlin/net/xintanalabs/rssotto/components/checker/scrape/ScrapeChecker.kt index 012f5c2..040edca 100644 --- a/src/main/kotlin/net/xintanalabs/rssotto/components/checker/scrape/ScrapeChecker.kt +++ b/src/main/kotlin/net/xintanalabs/rssotto/components/checker/scrape/ScrapeChecker.kt @@ -1,9 +1,10 @@ package net.xintanalabs.rssotto.components.checkers.scrape -import kotlinx.coroutines.delay +import net.xintanalabs.rssotto.components.checker.exceptions.CheckerException +import net.xintanalabs.rssotto.components.checker.scrape.FlaresSolverrFetcher import net.xintanalabs.rssotto.components.checkers.IVersionChecker -import net.xintanalabs.rssotto.model.Source -import net.xintanalabs.rssotto.tasks.ScheduledTasks +import net.xintanalabs.rssotto.model.SiteCache +import net.xintanalabs.rssotto.service.SiteCacheService import org.openqa.selenium.chrome.ChromeDriver import org.slf4j.LoggerFactory import org.springframework.stereotype.Component @@ -13,31 +14,82 @@ import java.util.regex.Pattern @Component class ScrapeChecker( private val restTemplate: RestTemplate, - private val chromeDriver: ChromeDriver + private val chromeDriver: ChromeDriver, + private val siteCacheService: SiteCacheService ) : IVersionChecker { private val log = LoggerFactory.getLogger(ScrapeChecker::class.java) - override suspend fun getLatestVersion(paramsDict: Map): String { - val url = paramsDict["url"]?.takeIf { it.isNotEmpty() } - ?: throw IllegalArgumentException("URL required") - log.info("Url : {}", url) - val mode = paramsDict["mode"] - log.info("Mode : {}", mode) - val fetcher: IScrapeFetcher = when (mode) { - "selenium" -> SeleniumFetcher(chromeDriver) - "jsoup" -> JSoupFetcher() - else -> DefaultScrapeFetcher(restTemplate) - } - val response = fetcher.fetch(url) - val cleanedResponse = response.replace(">\\s+<".toRegex(), "><") + override suspend fun getLatestVersion(paramsDict: Map): String? { + val info = mutableMapOf() + return try { + val url = paramsDict["url"]?.takeIf { it.isNotEmpty() } + ?: throw IllegalArgumentException("URL required") + info["url"] = url + val mode = paramsDict["mode"] + val cached: Boolean = paramsDict["cached"]?.toBoolean() ?: false + info["mode"] = mode ?: "" - val regex: String = paramsDict["regex"] as String - log.info("Regex : {}", regex) - val match = Pattern.compile(regex).matcher(cleanedResponse) - if (!match.find() || match.groupCount() < 1) { - throw Exception("No match with regex in response") + val fetcher: IScrapeFetcher = when (mode) { + "selenium" -> SeleniumFetcher(chromeDriver) + "jsoup" -> JSoupFetcher() + "flaresolverr" -> FlaresSolverrFetcher() + else -> DefaultScrapeFetcher(restTemplate) + } + + var response = "" + var hasCached = false + if (cached) { + val developerId = paramsDict["developerId"] ?: url + val siteCache = siteCacheService.getCacheByDeveloperId(developerId) + if (siteCache != null) { + if (siteCacheService.isCacheValid(siteCache)) { + response = siteCache.html + hasCached = true + } else { + siteCacheService.deleteById(siteCache.id ?: "") + } + } + } + if (!hasCached) { + response = cleanHtml(fetcher.fetch(url)).trim() + if (cached) { + val developerId = paramsDict["developerId"] ?: url + siteCacheService.create( + SiteCache( + developerId = developerId, + html = response, + createdAt = System.currentTimeMillis(), + expiresAt = System.currentTimeMillis() + (3600_000 * 5) // 5 hour + ) + ) + } + } + + if (response.isEmpty()) { + throw Exception("Empty response from URL") + } + + //val cleanedResponse = response.replace(">\\s+<".toRegex(), "><") + + val regex: String = paramsDict["regex"] as String + info["regex"] = regex + val match = Pattern.compile(regex).matcher(response) + if (!match.find() || match.groupCount() < 1) { + throw Exception("No match with regex in response") + } + match.group(1) + } catch (e: Exception) { + log.error("Error in ScrapeChecker: ${e.message}") + throw CheckerException(e.message, info, e) } - return match.group(1) + } + + private fun cleanHtml(html: String): String { + return html.replace(Regex(""), "") + .replace(Regex(""), "") + .replace(Regex(""), "") + .replace(Regex("\\s+"), " ") + .trim() } } \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/config/RssottoProperties.kt b/src/main/kotlin/net/xintanalabs/rssotto/config/RssottoProperties.kt new file mode 100644 index 0000000..d51ab79 --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/config/RssottoProperties.kt @@ -0,0 +1,8 @@ +package net.xintanalabs.rssotto.config + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties("rssotto") +data class RssottoProperties ( + val checkTaskCron: String = "*/45 * * * *" +) \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/config/SecurityConfiguration.kt b/src/main/kotlin/net/xintanalabs/rssotto/config/SecurityConfiguration.kt index 4d19816..d1ba304 100644 --- a/src/main/kotlin/net/xintanalabs/rssotto/config/SecurityConfiguration.kt +++ b/src/main/kotlin/net/xintanalabs/rssotto/config/SecurityConfiguration.kt @@ -9,12 +9,27 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe import org.springframework.security.config.http.SessionCreationPolicy import org.springframework.security.web.DefaultSecurityFilterChain import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter +import org.springframework.web.cors.CorsConfiguration +import org.springframework.web.cors.UrlBasedCorsConfigurationSource +import org.springframework.web.filter.CorsFilter @Configuration @EnableWebSecurity class SecurityConfiguration( private val authenticationProvider: AuthenticationProvider ) { + @Bean + fun corsConfigurationSource(): UrlBasedCorsConfigurationSource { + val config = CorsConfiguration() + config.allowedOrigins = listOf("http://localhost:4200") + config.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS") + config.allowedHeaders = listOf("*") + config.allowCredentials = true + + val source = UrlBasedCorsConfigurationSource() + source.registerCorsConfiguration("/**", config) + return source + } @Bean fun securityFilterChain( @@ -22,10 +37,11 @@ class SecurityConfiguration( jwtAuthenticationFilter: JwtAuthenticationFilter ): DefaultSecurityFilterChain = http + .cors { } .csrf { it.disable() } .authorizeHttpRequests { it - .requestMatchers("/api/auth", "/api/auth/refresh", "/error") + .requestMatchers("/api/auth/**", "/error") .permitAll() .requestMatchers(HttpMethod.POST, "/api/user") .permitAll() @@ -40,4 +56,5 @@ class SecurityConfiguration( .authenticationProvider(authenticationProvider) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java) .build() -} \ No newline at end of file +} + diff --git a/src/main/kotlin/net/xintanalabs/rssotto/constants/Constants.kt b/src/main/kotlin/net/xintanalabs/rssotto/constants/Constants.kt index f087f27..31b0edb 100644 --- a/src/main/kotlin/net/xintanalabs/rssotto/constants/Constants.kt +++ b/src/main/kotlin/net/xintanalabs/rssotto/constants/Constants.kt @@ -6,4 +6,7 @@ object Constants { const val COLLECTION_APPS: String = "apps" const val COLLECTION_SOURCES: String = "sources" const val COLLECTION_CHECKER_TYPES: String = "checkerTypes" + const val COLLECTION_NOTIFICATIONS: String = "notifications" + const val COLLECTION_DEVELOPERS: String = "developers" + const val COLLECTION_SITE_CACHE: String = "siteCache" } \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/controller/actions/ActionsController.kt b/src/main/kotlin/net/xintanalabs/rssotto/controller/actions/ActionsController.kt new file mode 100644 index 0000000..51c1202 --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/controller/actions/ActionsController.kt @@ -0,0 +1,25 @@ +package net.xintanalabs.rssotto.controller.actions + +import net.xintanalabs.rssotto.constants.Constants +import net.xintanalabs.rssotto.controller.app.AppVersionResponse +import net.xintanalabs.rssotto.service.ActionsService +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("${Constants.API_BASE_PATH}/exec") +class ActionsController( + private val actionsService: ActionsService +){ + @PostMapping("/check") + fun checkApps(@RequestParam(name = "id") idList: List): List { + return actionsService.checkVersions(idList) + } + + @PostMapping("/check-all") + fun checkAllApps(): List { + return actionsService.checkAllVersions() + } +} diff --git a/src/main/kotlin/net/xintanalabs/rssotto/controller/app/AppController.kt b/src/main/kotlin/net/xintanalabs/rssotto/controller/app/AppController.kt index a8efeef..a13dec1 100644 --- a/src/main/kotlin/net/xintanalabs/rssotto/controller/app/AppController.kt +++ b/src/main/kotlin/net/xintanalabs/rssotto/controller/app/AppController.kt @@ -1,6 +1,7 @@ package net.xintanalabs.rssotto.controller.app import net.xintanalabs.rssotto.constants.Constants +import net.xintanalabs.rssotto.enums.ModType import net.xintanalabs.rssotto.model.App import net.xintanalabs.rssotto.service.AppService import org.springframework.http.HttpStatus @@ -36,14 +37,15 @@ class AppController( appService.getAllApps() .map { it.toResponse() } - @GetMapping("/versions") - fun getAppsVersions(@RequestParam version: List): List { - return appService.getAppsByVersions(version).map { it.toVersionResponse() } - } - @GetMapping("/search") - fun searchApps(@RequestParam q: String): List { - return appService.searchApps(q).map { it.toResponse() } + fun searchApps(@RequestParam q: String?, @RequestParam id: List?): List { + if (id != null && id.isNotEmpty()) { + return appService.getAppsByIds(id).map { it.toResponse() } + } + if (!q.isNullOrBlank()) { + return appService.searchApps(q).map { it.toResponse() } + } + return emptyList() } private fun App.toVersionResponse(): AppVersionResponse { @@ -56,7 +58,6 @@ class AppController( private fun App.toResponse(): AppResponse { return AppResponse( id = this.id, - uid = this.uid, name = this.name, type = this.type, source = this.source, @@ -67,13 +68,14 @@ class AppController( createdAt = this.createdAt, updatedAt = this.updatedAt, lastCheckedAt = this.lastCheckedAt, - active = this.active + active = this.active, + developer = this.developer, + modType = this.modType ?: ModType.NOT_SET ) } private fun AppRequest.toModel(): App { return App( - uid = this.uid, name = this.name, type = this.type, source = this.source, @@ -84,7 +86,9 @@ class AppController( createdAt = System.currentTimeMillis(), updatedAt = System.currentTimeMillis(), lastCheckedAt = 0, - active = true + active = true, + developer = this.developer, + modType = this.modType ) } } \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/controller/app/AppRequest.kt b/src/main/kotlin/net/xintanalabs/rssotto/controller/app/AppRequest.kt index e46c7c4..7aad513 100644 --- a/src/main/kotlin/net/xintanalabs/rssotto/controller/app/AppRequest.kt +++ b/src/main/kotlin/net/xintanalabs/rssotto/controller/app/AppRequest.kt @@ -1,7 +1,8 @@ package net.xintanalabs.rssotto.controller.app +import net.xintanalabs.rssotto.enums.ModType + data class AppRequest( - val uid: String, val name: String, val type: String = "", val source: String, @@ -12,5 +13,7 @@ data class AppRequest( val createdAt: Long = 0, val updatedAt: Long = 0, val lastCheckedAt: Long = 0, - val active: Boolean = true + val active: Boolean = true, + val developer: String? = null, + val modType: ModType ) diff --git a/src/main/kotlin/net/xintanalabs/rssotto/controller/app/AppResponse.kt b/src/main/kotlin/net/xintanalabs/rssotto/controller/app/AppResponse.kt index 0392fda..c687cd7 100644 --- a/src/main/kotlin/net/xintanalabs/rssotto/controller/app/AppResponse.kt +++ b/src/main/kotlin/net/xintanalabs/rssotto/controller/app/AppResponse.kt @@ -1,8 +1,9 @@ package net.xintanalabs.rssotto.controller.app +import net.xintanalabs.rssotto.enums.ModType + data class AppResponse( val id: String? = null, - val uid: String, val name: String, val type: String = "", val source: String, @@ -13,5 +14,7 @@ data class AppResponse( val createdAt: Long = 0, val updatedAt: Long = 0, val lastCheckedAt: Long = 0, - val active: Boolean = true + val active: Boolean = true, + val developer: String? = null, + val modType: ModType ) diff --git a/src/main/kotlin/net/xintanalabs/rssotto/controller/checkerType/CheckerTypeController.kt b/src/main/kotlin/net/xintanalabs/rssotto/controller/checkerType/CheckerTypeController.kt new file mode 100644 index 0000000..5409063 --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/controller/checkerType/CheckerTypeController.kt @@ -0,0 +1,54 @@ +package net.xintanalabs.rssotto.controller.checkerType + +import net.xintanalabs.rssotto.constants.Constants +import net.xintanalabs.rssotto.model.CheckerType +import net.xintanalabs.rssotto.service.CheckerTypeService +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + + +@RestController +@RequestMapping("${Constants.API_BASE_PATH}/checker-type") +class CheckerTypeController( + private val checkerTypeService: CheckerTypeService +) { + @PostMapping + fun createCheckerType(@RequestBody checkerType: CheckerTypeRequest): CheckerTypeResponse { + return checkerTypeService.create(checkerType.toModel()).toResponse() + } + + @GetMapping + fun getAllCheckerTypes(): List { + try { + return checkerTypeService.findAll().map { it.toResponse() } + } catch (e: InterruptedException) { + throw e + } + + } + + @GetMapping("/{id}") + fun getType(@PathVariable id: String): CheckerTypeResponse? { + return checkerTypeService.findById(id).toResponse() + } + + private fun CheckerType?.toResponse(): CheckerTypeResponse = + CheckerTypeResponse( + id = this?.id, + name = this?.name ?: "", + params = this?.params ?: listOf() + ) + + private fun CheckerTypeRequest.toModel(): CheckerType = + CheckerType( + id = null, + name = this.name, + params = this.params + ) +} + + diff --git a/src/main/kotlin/net/xintanalabs/rssotto/controller/checkerType/CheckerTypeRequest.kt b/src/main/kotlin/net/xintanalabs/rssotto/controller/checkerType/CheckerTypeRequest.kt new file mode 100644 index 0000000..5dc344d --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/controller/checkerType/CheckerTypeRequest.kt @@ -0,0 +1,8 @@ +package net.xintanalabs.rssotto.controller.checkerType + +import net.xintanalabs.rssotto.model.Field + +data class CheckerTypeRequest ( + val name: String, + val params: List +) diff --git a/src/main/kotlin/net/xintanalabs/rssotto/controller/checkerType/CheckerTypeResponse.kt b/src/main/kotlin/net/xintanalabs/rssotto/controller/checkerType/CheckerTypeResponse.kt new file mode 100644 index 0000000..8a04f81 --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/controller/checkerType/CheckerTypeResponse.kt @@ -0,0 +1,9 @@ +package net.xintanalabs.rssotto.controller.checkerType + +import net.xintanalabs.rssotto.model.Field + +data class CheckerTypeResponse ( + val id: String? = null, + val name: String, + val params: List +) diff --git a/src/main/kotlin/net/xintanalabs/rssotto/controller/developer/DeveloperController.kt b/src/main/kotlin/net/xintanalabs/rssotto/controller/developer/DeveloperController.kt new file mode 100644 index 0000000..b40ab63 --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/controller/developer/DeveloperController.kt @@ -0,0 +1,59 @@ +package net.xintanalabs.rssotto.controller.developer + +import net.xintanalabs.rssotto.constants.Constants +import net.xintanalabs.rssotto.model.Developer +import net.xintanalabs.rssotto.service.DeveloperService +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.server.ResponseStatusException + +@RestController +@RequestMapping("${Constants.API_BASE_PATH}/developer") +class DeveloperController( + private val developerService: DeveloperService +) { + @PostMapping + fun create(@RequestBody developerRequest: DeveloperRequest): DeveloperResponse { + return try { + developerService.create(developerRequest.toModel()) + .toResponse() + } catch (e: Exception) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot create developer: ${e.message}", e) + } + } + + @GetMapping + fun getAll(): List { + return try { + developerService.getAll() + .map { it.toResponse() } + } catch (e: Exception) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot get developers: ${e.message}", e) + } + } + + @GetMapping("/{id}") + fun getById(@PathVariable id: String): DeveloperResponse { + return developerService.getById(id) + ?.toResponse() + ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Developer not found") + } + + private fun Developer.toResponse(): DeveloperResponse = DeveloperResponse( + id = this.id ?: "", + name = this.name, + legalName = this.legalName, + webUrl = this.webUrl, + ) + + private fun DeveloperRequest.toModel(): Developer = Developer( + name = this.name, + legalName = this.legalName, + webUrl = this.webUrl, + ) +} diff --git a/src/main/kotlin/net/xintanalabs/rssotto/controller/developer/DeveloperRequest.kt b/src/main/kotlin/net/xintanalabs/rssotto/controller/developer/DeveloperRequest.kt new file mode 100644 index 0000000..b35774e --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/controller/developer/DeveloperRequest.kt @@ -0,0 +1,7 @@ +package net.xintanalabs.rssotto.controller.developer + +data class DeveloperRequest( + val name: String, + val legalName: String, + val webUrl: String +) diff --git a/src/main/kotlin/net/xintanalabs/rssotto/controller/developer/DeveloperResponse.kt b/src/main/kotlin/net/xintanalabs/rssotto/controller/developer/DeveloperResponse.kt new file mode 100644 index 0000000..55ee694 --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/controller/developer/DeveloperResponse.kt @@ -0,0 +1,8 @@ +package net.xintanalabs.rssotto.controller.developer + +data class DeveloperResponse( + val id: String, + val name: String, + val legalName: String, + val webUrl: String +) diff --git a/src/main/kotlin/net/xintanalabs/rssotto/controller/source/SourceController.kt b/src/main/kotlin/net/xintanalabs/rssotto/controller/source/SourceController.kt index 8629d17..06267f0 100644 --- a/src/main/kotlin/net/xintanalabs/rssotto/controller/source/SourceController.kt +++ b/src/main/kotlin/net/xintanalabs/rssotto/controller/source/SourceController.kt @@ -15,7 +15,7 @@ import org.springframework.web.server.ResponseStatusException @RequestMapping("${Constants.API_BASE_PATH}/source") class SourceController(private val sourceService: SourceService) { @PostMapping - suspend fun create(@RequestBody sourceRequest: SourceRequest): SourceResponse { + fun create(@RequestBody sourceRequest: SourceRequest): SourceResponse { return try { sourceService.create(sourceRequest.toModel()) .toResponse() diff --git a/src/main/kotlin/net/xintanalabs/rssotto/controller/user/UserController.kt b/src/main/kotlin/net/xintanalabs/rssotto/controller/user/UserController.kt index c8ace96..18a04d1 100644 --- a/src/main/kotlin/net/xintanalabs/rssotto/controller/user/UserController.kt +++ b/src/main/kotlin/net/xintanalabs/rssotto/controller/user/UserController.kt @@ -1,6 +1,6 @@ package net.xintanalabs.rssotto.controller.user -import net.xintanalabs.rssotto.model.Role +import net.xintanalabs.rssotto.enums.Role import net.xintanalabs.rssotto.model.User import net.xintanalabs.rssotto.service.UserService import org.springframework.http.HttpStatus diff --git a/src/main/kotlin/net/xintanalabs/rssotto/db/mongodb/MongoDBAbstract.kt b/src/main/kotlin/net/xintanalabs/rssotto/db/mongodb/MongoDBAbstract.kt index 6b6310c..f9fc61f 100644 --- a/src/main/kotlin/net/xintanalabs/rssotto/db/mongodb/MongoDBAbstract.kt +++ b/src/main/kotlin/net/xintanalabs/rssotto/db/mongodb/MongoDBAbstract.kt @@ -1,6 +1,5 @@ package net.xintanalabs.rssotto.db.mongodb -import org.springframework.beans.factory.annotation.Autowired import org.springframework.data.mongodb.core.query.Criteria abstract class MongoDBAbstract ( @@ -10,27 +9,31 @@ abstract class MongoDBAbstract ( protected abstract val entityClass: Class protected val idField: String = "id" - protected fun create(entity: T): T { + protected fun createEntity(entity: T): T { return mongoDBClient.insert(collection, entity, entityClass) } - protected fun getAll(): List { + protected fun getAllEntities(): List { return mongoDBClient.findAll(collection, entityClass) } - protected fun getById(id: String): T? { + protected fun deleteEntitiesByCriteria(criteria: Criteria): Long { + return mongoDBClient.deleteMany(collection, criteria, entityClass) + } + + protected fun getEntityById(id: String): T? { return mongoDBClient.findOne(collection, idField, id, entityClass) } - protected fun delete(id: String): Long { + protected fun deleteEntity(id: String): Long { return mongoDBClient.deleteOne(collection, idField, id, entityClass) } - protected fun update(id: String, updateFields: Map): Long { + protected fun updateEntity(id: String, updateFields: Map): Long { return mongoDBClient.updateOne(collection, idField, id, updateFields, entityClass) } - protected fun findByCriteria(criteria: Criteria): List { + protected fun findEntitiesByCriteria(criteria: Criteria): List { return mongoDBClient.findByFilter(collection, criteria, entityClass) } } \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/db/mongodb/MongoDBClient.kt b/src/main/kotlin/net/xintanalabs/rssotto/db/mongodb/MongoDBClient.kt index 0ff3a77..ab61bad 100644 --- a/src/main/kotlin/net/xintanalabs/rssotto/db/mongodb/MongoDBClient.kt +++ b/src/main/kotlin/net/xintanalabs/rssotto/db/mongodb/MongoDBClient.kt @@ -54,6 +54,11 @@ class MongoDBClient(private val mongoTemplate: MongoTemplate) { return mongoTemplate.remove(query, clazz, collectionName).deletedCount } + fun deleteMany(collectionName: String, criteria: Criteria, clazz: Class): Long { + val query = Query(criteria) + return mongoTemplate.remove(query, clazz, collectionName).deletedCount + } + fun aggregate( collectionName: String, aggregation: Aggregation, diff --git a/src/main/kotlin/net/xintanalabs/rssotto/enums/ModType.kt b/src/main/kotlin/net/xintanalabs/rssotto/enums/ModType.kt new file mode 100644 index 0000000..0229f4e --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/enums/ModType.kt @@ -0,0 +1,5 @@ +package net.xintanalabs.rssotto.enums + +enum class ModType { + AIRCRAFT, AIRPORT, SCENERY, DESKTOP, UTILITY, OTHER, NOT_SET +} \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/enums/NotificationType.kt b/src/main/kotlin/net/xintanalabs/rssotto/enums/NotificationType.kt new file mode 100644 index 0000000..f150291 --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/enums/NotificationType.kt @@ -0,0 +1,9 @@ +package net.xintanalabs.rssotto.enums + +enum class NotificationType { + VERSION_MISMATCH, + TIMEOUT, + UNREACHABLE, + CHECK_FAILED, + ONE_TIME_TASK_MISCONFIG +} \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/enums/Role.kt b/src/main/kotlin/net/xintanalabs/rssotto/enums/Role.kt new file mode 100644 index 0000000..321adba --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/enums/Role.kt @@ -0,0 +1,5 @@ +package net.xintanalabs.rssotto.enums + +enum class Role { + USER, ADMIN +} \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/enums/Severity.kt b/src/main/kotlin/net/xintanalabs/rssotto/enums/Severity.kt new file mode 100644 index 0000000..c2d9cb1 --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/enums/Severity.kt @@ -0,0 +1,5 @@ +package net.xintanalabs.rssotto.enums + +enum class Severity { + LOW, MEDIUM, HIGH, CRITICAL +} \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/model/App.kt b/src/main/kotlin/net/xintanalabs/rssotto/model/App.kt index 4d11a81..ae2cdb8 100644 --- a/src/main/kotlin/net/xintanalabs/rssotto/model/App.kt +++ b/src/main/kotlin/net/xintanalabs/rssotto/model/App.kt @@ -1,13 +1,13 @@ package net.xintanalabs.rssotto.model import com.fasterxml.jackson.annotation.JsonInclude +import net.xintanalabs.rssotto.enums.ModType import org.springframework.data.annotation.Id @JsonInclude(JsonInclude.Include.NON_NULL) data class App( @Id val id: String? = null, - val uid: String, val name: String, val type: String = "", val source: String, @@ -18,5 +18,7 @@ data class App( val createdAt: Long = 0, val updatedAt: Long = 0, val lastCheckedAt: Long = 0, - val active: Boolean = true + val active: Boolean = true, + val developer: String? = null, + val modType: ModType? = null ) diff --git a/src/main/kotlin/net/xintanalabs/rssotto/model/CheckerType.kt b/src/main/kotlin/net/xintanalabs/rssotto/model/CheckerType.kt new file mode 100644 index 0000000..4a4ce93 --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/model/CheckerType.kt @@ -0,0 +1,11 @@ +package net.xintanalabs.rssotto.model + +import com.fasterxml.jackson.annotation.JsonInclude +import org.springframework.data.annotation.Id + +@JsonInclude(JsonInclude.Include.NON_NULL) +data class CheckerType( + @Id val id: String? = null, + val name: String, + val params: List +) diff --git a/src/main/kotlin/net/xintanalabs/rssotto/model/Developer.kt b/src/main/kotlin/net/xintanalabs/rssotto/model/Developer.kt new file mode 100644 index 0000000..3633327 --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/model/Developer.kt @@ -0,0 +1,10 @@ +package net.xintanalabs.rssotto.model + +import org.springframework.data.annotation.Id + +data class Developer( + @Id val id: String? = null, + val name: String, + val legalName: String, + val webUrl: String +) diff --git a/src/main/kotlin/net/xintanalabs/rssotto/model/Notification.kt b/src/main/kotlin/net/xintanalabs/rssotto/model/Notification.kt new file mode 100644 index 0000000..604b57d --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/model/Notification.kt @@ -0,0 +1,17 @@ +package net.xintanalabs.rssotto.model + +import net.xintanalabs.rssotto.enums.NotificationType +import net.xintanalabs.rssotto.enums.Severity + +data class Notification( + val id: String? = null, + val appId: String? = null, + val title: String, + val type: NotificationType, + val message: String, + val description: String? = null, + val stackTrace: String? = null, + val severity: Severity, + val createdAt: Long = 0, + val read: Boolean = false +) diff --git a/src/main/kotlin/net/xintanalabs/rssotto/model/SiteCache.kt b/src/main/kotlin/net/xintanalabs/rssotto/model/SiteCache.kt new file mode 100644 index 0000000..e08d8f1 --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/model/SiteCache.kt @@ -0,0 +1,11 @@ +package net.xintanalabs.rssotto.model + +import org.springframework.data.annotation.Id + +data class SiteCache( + @Id val id: String? = null, + val developerId: String, + val html: String, + val createdAt: Long = 0, + val expiresAt: Long = 0 +) diff --git a/src/main/kotlin/net/xintanalabs/rssotto/model/User.kt b/src/main/kotlin/net/xintanalabs/rssotto/model/User.kt index 42a3bb2..11624eb 100644 --- a/src/main/kotlin/net/xintanalabs/rssotto/model/User.kt +++ b/src/main/kotlin/net/xintanalabs/rssotto/model/User.kt @@ -1,5 +1,6 @@ package net.xintanalabs.rssotto.model +import net.xintanalabs.rssotto.enums.Role import java.util.UUID data class User( @@ -7,9 +8,4 @@ data class User( val username: String, val password: String, val role: Role -) - - -enum class Role { - USER, ADMIN -} \ No newline at end of file +) \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/repository/AppRepository.kt b/src/main/kotlin/net/xintanalabs/rssotto/repository/AppRepository.kt index 3d48903..df0bb22 100644 --- a/src/main/kotlin/net/xintanalabs/rssotto/repository/AppRepository.kt +++ b/src/main/kotlin/net/xintanalabs/rssotto/repository/AppRepository.kt @@ -15,26 +15,26 @@ class AppRepository( override val entityClass: Class = App::class.java fun createApp(app: App): App { - return create(app) + return createEntity(app) } fun getAllApps(): List { - return getAll() + return getAllEntities() } fun getAppById(id: String): App? { - return getById(id) + return getEntityById(id) } fun deleteApp(id: String): Long { - return delete(id) + return deleteEntity(id) } fun updateApp(id: String, updateFields: Map): Long { - return update(id, updateFields) + return updateEntity(id, updateFields) } fun findAppsByCriteria(criteria: Criteria): List { - return findByCriteria(criteria) + return findEntitiesByCriteria(criteria) } } \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/repository/CheckerTypeRepository.kt b/src/main/kotlin/net/xintanalabs/rssotto/repository/CheckerTypeRepository.kt new file mode 100644 index 0000000..e5d2153 --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/repository/CheckerTypeRepository.kt @@ -0,0 +1,32 @@ +package net.xintanalabs.rssotto.repository + +import net.xintanalabs.rssotto.constants.Constants +import net.xintanalabs.rssotto.db.mongodb.MongoDBAbstract +import net.xintanalabs.rssotto.db.mongodb.MongoDBClient +import net.xintanalabs.rssotto.model.CheckerType +import org.springframework.stereotype.Repository + +@Repository +class CheckerTypeRepository( + mongoDBClient: MongoDBClient +): MongoDBAbstract(mongoDBClient) { + override val collection: String = Constants.COLLECTION_CHECKER_TYPES + override val entityClass: Class = CheckerType::class.java + + fun createCheckerType(checkerType: CheckerType): CheckerType { + return createEntity(checkerType) + } + + fun getAllCheckerTypes(): List { + return try { + getAllEntities() + } catch (e: Exception) { + println("Error fetching all checker types: ${e.message}") + throw e + } + } + + fun getCheckerTypeById(id: String): CheckerType? { + return getEntityById(id) + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/repository/DeveloperRepository.kt b/src/main/kotlin/net/xintanalabs/rssotto/repository/DeveloperRepository.kt new file mode 100644 index 0000000..bea41fa --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/repository/DeveloperRepository.kt @@ -0,0 +1,27 @@ +package net.xintanalabs.rssotto.repository + +import net.xintanalabs.rssotto.constants.Constants +import net.xintanalabs.rssotto.db.mongodb.MongoDBAbstract +import net.xintanalabs.rssotto.db.mongodb.MongoDBClient +import net.xintanalabs.rssotto.model.Developer +import org.springframework.stereotype.Repository + +@Repository +class DeveloperRepository( + mongoDBClient: MongoDBClient +): MongoDBAbstract(mongoDBClient) { + override val collection: String = Constants.COLLECTION_DEVELOPERS + override val entityClass: Class = Developer::class.java + + fun create(developer: Developer): Developer { + return createEntity(developer) + } + + fun getAll(): List { + return getAllEntities() + } + + fun getById(id: String): Developer? { + return getEntityById(id) + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/repository/NotificationRepository.kt b/src/main/kotlin/net/xintanalabs/rssotto/repository/NotificationRepository.kt new file mode 100644 index 0000000..cef7eff --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/repository/NotificationRepository.kt @@ -0,0 +1,31 @@ +package net.xintanalabs.rssotto.repository + +import net.xintanalabs.rssotto.constants.Constants +import net.xintanalabs.rssotto.db.mongodb.MongoDBAbstract +import net.xintanalabs.rssotto.db.mongodb.MongoDBClient +import net.xintanalabs.rssotto.model.Notification +import org.springframework.data.mongodb.core.query.Criteria +import org.springframework.stereotype.Repository + +@Repository +class NotificationRepository( + mongoDBClient: MongoDBClient +): MongoDBAbstract(mongoDBClient) { + override val collection: String = Constants.COLLECTION_NOTIFICATIONS + override val entityClass: Class = Notification::class.java + + fun createNotification(notification: Notification): Notification { + return createEntity(notification) + } + + fun getAllNotifications(): List { + return getAllEntities() + } + fun getNotificationById(id: String): Notification? { + return getEntityById(id) + } + fun getNotificationByFilter(criteria: Criteria): List { + return findEntitiesByCriteria(criteria) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/repository/SiteCacheRepository.kt b/src/main/kotlin/net/xintanalabs/rssotto/repository/SiteCacheRepository.kt new file mode 100644 index 0000000..0813e89 --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/repository/SiteCacheRepository.kt @@ -0,0 +1,33 @@ +package net.xintanalabs.rssotto.repository + +import net.xintanalabs.rssotto.constants.Constants +import net.xintanalabs.rssotto.db.mongodb.MongoDBAbstract +import net.xintanalabs.rssotto.db.mongodb.MongoDBClient +import net.xintanalabs.rssotto.model.SiteCache +import org.springframework.data.mongodb.core.query.Criteria +import org.springframework.stereotype.Repository + +@Repository +class SiteCacheRepository( + mongoDBClient: MongoDBClient +): MongoDBAbstract(mongoDBClient) { + override val collection = Constants.COLLECTION_SITE_CACHE + override val entityClass = SiteCache::class.java + + fun create(siteCache: SiteCache): SiteCache { + return createEntity(siteCache) + } + + fun getByDeveloperId(developerId: String): SiteCache? { + val criteria = Criteria.where("developerId").`is`(developerId) + return findEntitiesByCriteria(criteria).firstOrNull() + } + + fun deleteAll(): Long { + return deleteEntitiesByCriteria(Criteria()) + } + + fun deleteById(id: String): Long { + return deleteEntity(id) + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/repository/SourceRepository.kt b/src/main/kotlin/net/xintanalabs/rssotto/repository/SourceRepository.kt index 11a0ff9..bbec212 100644 --- a/src/main/kotlin/net/xintanalabs/rssotto/repository/SourceRepository.kt +++ b/src/main/kotlin/net/xintanalabs/rssotto/repository/SourceRepository.kt @@ -4,7 +4,6 @@ import net.xintanalabs.rssotto.constants.Constants import net.xintanalabs.rssotto.db.mongodb.MongoDBAbstract import net.xintanalabs.rssotto.db.mongodb.MongoDBClient import net.xintanalabs.rssotto.model.Source -import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Repository @Repository @@ -15,15 +14,15 @@ class SourceRepository ( override val entityClass: Class = Source::class.java fun createSource(source: Source): Source { - return create(source) + return createEntity(source) } fun getAllSources(): List { - return getAll() + return getAllEntities() } fun getSourceById(id: String): Source? { - return getById(id) + return getEntityById(id) } } diff --git a/src/main/kotlin/net/xintanalabs/rssotto/repository/TypeRepository.kt b/src/main/kotlin/net/xintanalabs/rssotto/repository/TypeRepository.kt index 9411aeb..995e7d3 100644 --- a/src/main/kotlin/net/xintanalabs/rssotto/repository/TypeRepository.kt +++ b/src/main/kotlin/net/xintanalabs/rssotto/repository/TypeRepository.kt @@ -13,15 +13,15 @@ class TypeRepository( override val collection: String = Constants.COLLECTION_TYPES override val entityClass: Class = Type::class.java - fun createType(type: Type): Type { - return create(type) + fun create(type: Type): Type { + return createEntity(type) } - fun getAllTypes(): List { - return getAll() + fun getAll(): List { + return getAllEntities() } - fun getTypeById(id: String): Type? { - return getById(id) + fun getById(id: String): Type? { + return getEntityById(id) } } \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/repository/UserRepository.kt b/src/main/kotlin/net/xintanalabs/rssotto/repository/UserRepository.kt index b99895c..c41a2f2 100644 --- a/src/main/kotlin/net/xintanalabs/rssotto/repository/UserRepository.kt +++ b/src/main/kotlin/net/xintanalabs/rssotto/repository/UserRepository.kt @@ -1,6 +1,6 @@ package net.xintanalabs.rssotto.repository -import net.xintanalabs.rssotto.model.Role +import net.xintanalabs.rssotto.enums.Role import net.xintanalabs.rssotto.model.User import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Repository diff --git a/src/main/kotlin/net/xintanalabs/rssotto/service/ActionsService.kt b/src/main/kotlin/net/xintanalabs/rssotto/service/ActionsService.kt new file mode 100644 index 0000000..cbb1215 --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/service/ActionsService.kt @@ -0,0 +1,127 @@ +package net.xintanalabs.rssotto.service + +import kotlinx.coroutines.runBlocking +import net.xintanalabs.rssotto.components.checker.exceptions.CheckerException +import net.xintanalabs.rssotto.components.checkers.CheckerFactory +import net.xintanalabs.rssotto.controller.app.AppVersionResponse +import net.xintanalabs.rssotto.enums.NotificationType +import net.xintanalabs.rssotto.enums.Severity +import net.xintanalabs.rssotto.model.App +import net.xintanalabs.rssotto.model.CheckerType +import net.xintanalabs.rssotto.model.Notification +import net.xintanalabs.rssotto.model.Source +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class ActionsService( + private val checkerFactory: CheckerFactory, + private val appService: AppService, + private val sourceService: SourceService, + private val checkerTypeService: CheckerTypeService, + private val notificationService: NotificationService +) { + + private val log = LoggerFactory.getLogger(ActionsService::class.java) + + fun checkVersions(idList: List): List { + val apps = appService.getAppsByIds(idList) + return checkApps(apps) + } + + fun checkAllVersions(): List { + val apps = appService.getAllApps() + return checkApps(apps) + } + + private fun checkApps(apps: List): List { + val sources = sourceService.findALl() + val checkerTypes = checkerTypeService.findAll() + val versionList = mutableListOf() + + apps.forEach { + app -> + if (app.id != null && app.active) { + val appLatestVersion: String? = getLatestAppVersion(app, sources, checkerTypes) + versionList.add( + AppVersionResponse( + id = app.id, + latestVersion = appLatestVersion + ) + ) + } + } + + return versionList + } + + private fun getLatestAppVersion( + app: App, + sources: List, + checkerTypes: List + ): String? { + val source: Source? = sources.find { s -> s.id == app.source } + var appVersion: String? = null + + if (source != null) { + val checkerType = checkerTypes.find { ct -> ct.id == source.checkerType } + if (checkerType != null) { + val checker = checkerFactory.createChecker(checkerType.name) + val paramsMap = getParamsMap(app, checkerType, source); + appVersion = runBlocking { + try { + checker.getLatestVersion(paramsMap) + } + catch (e: CheckerException) { + val notification = Notification( + appId = app.id, + title = "Error checking version for app ${app.name}", + type = NotificationType.CHECK_FAILED, + message = e.message?:"Unknown error", + description = e.info?.entries?.joinToString("\n") { "${it.key}: ${it.value}" }, + stackTrace = e.stackTrace.joinToString("\n"), + severity = Severity.HIGH, + createdAt = System.currentTimeMillis() + ) + notificationService.create(notification) + log.error("Error checking version for app ${app.name}: ${e.message}") + null + } + } + } + if (appVersion != null) { + log.info("App (${app.name}, latest versión ${appVersion}") + appService.updateLatestVersion(app.id!!, appVersion) + } + } + return appVersion + } + + private fun getParamsMap(app: App, checkerType: CheckerType, source: Source): Map { + val paramsMap = mutableMapOf() + checkerType.params.forEach { field -> + val value = getValue(field.name, app, source) + paramsMap[field.name] = value + } + return additionalParams(paramsMap, app) + + } + + private fun additionalParams(map: MutableMap, app: App): Map { + if (map.keys.contains("cached")) { + map["developerId"] = app.developer ?: "" + } + return map + } + + private fun getValue(field: String, app: App, source: Source): String { + if (field == "id") { + return app.id ?: "" + } + var value: String? = app.params[field] + if (value == null) { + value = source.defaults[field] + } + return value ?: "" + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/service/AppService.kt b/src/main/kotlin/net/xintanalabs/rssotto/service/AppService.kt index 37e0a90..7267691 100644 --- a/src/main/kotlin/net/xintanalabs/rssotto/service/AppService.kt +++ b/src/main/kotlin/net/xintanalabs/rssotto/service/AppService.kt @@ -23,6 +23,16 @@ class AppService( return appRepository.getAppById(id) } + fun getAppsByIds(ids: List): List { + val criteria = Criteria.where("_id").`in`(ids) + return appRepository.findAppsByCriteria(criteria) + } + + fun getAppsByVersions(versions: List): List { + val criteria = Criteria.where("latestVersion").`in`(versions) + return appRepository.findAppsByCriteria(criteria) + } + fun deleteApp(id: String): Long { return appRepository.deleteApp(id) } @@ -39,17 +49,13 @@ class AppService( } fun updateLatestVersion(appId: String, latestVersion: String): Long { - val updateFields = mapOf( - "latestVersion" to latestVersion + val updateFields = mapOf( + "latestVersion" to latestVersion, + "lastCheckedAt" to System.currentTimeMillis() ) return appRepository.updateApp(appId, updateFields) } - fun getAppsByVersions(versions: List): List { - val criteria = Criteria.where("latestVersion").`in`(versions) - return appRepository.findAppsByCriteria(criteria) - } - fun searchApps(query: String): List { val regex = ".*${Regex.escape(query)}.*" val criteria = Criteria.where("name").regex(regex, "i") diff --git a/src/main/kotlin/net/xintanalabs/rssotto/service/CheckerTypeService.kt b/src/main/kotlin/net/xintanalabs/rssotto/service/CheckerTypeService.kt new file mode 100644 index 0000000..af7b956 --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/service/CheckerTypeService.kt @@ -0,0 +1,22 @@ +package net.xintanalabs.rssotto.service + +import net.xintanalabs.rssotto.model.CheckerType +import net.xintanalabs.rssotto.repository.CheckerTypeRepository +import org.springframework.stereotype.Service + +@Service +class CheckerTypeService( + private val checkerTypeRepository: CheckerTypeRepository +) { + fun findAll(): List { + return checkerTypeRepository.getAllCheckerTypes() + } + + fun findById(id: String): CheckerType? { + return checkerTypeRepository.getCheckerTypeById(id) + } + + fun create(checkerType: CheckerType): CheckerType? { + return checkerTypeRepository.createCheckerType(checkerType) + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/service/DeveloperService.kt b/src/main/kotlin/net/xintanalabs/rssotto/service/DeveloperService.kt new file mode 100644 index 0000000..e0c69e8 --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/service/DeveloperService.kt @@ -0,0 +1,22 @@ +package net.xintanalabs.rssotto.service + +import net.xintanalabs.rssotto.model.Developer +import net.xintanalabs.rssotto.repository.DeveloperRepository +import org.springframework.stereotype.Service + +@Service +class DeveloperService( + private val developerRepository: DeveloperRepository +) { + fun getAll(): List { + return developerRepository.getAll() + } + + fun getById(id: String): Developer? { + return developerRepository.getById(id) + } + + fun create(developer: Developer): Developer { + return developerRepository.create(developer) + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/service/NotificationService.kt b/src/main/kotlin/net/xintanalabs/rssotto/service/NotificationService.kt new file mode 100644 index 0000000..5813555 --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/service/NotificationService.kt @@ -0,0 +1,17 @@ +package net.xintanalabs.rssotto.service + +import net.xintanalabs.rssotto.model.Notification +import net.xintanalabs.rssotto.repository.NotificationRepository +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class NotificationService( + private val notificationRepository: NotificationRepository +) { + private val log = LoggerFactory.getLogger(NotificationService::class.java) + + fun create(notification: Notification): Notification { + return notificationRepository.createNotification(notification) + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/service/SiteCacheService.kt b/src/main/kotlin/net/xintanalabs/rssotto/service/SiteCacheService.kt new file mode 100644 index 0000000..74d2829 --- /dev/null +++ b/src/main/kotlin/net/xintanalabs/rssotto/service/SiteCacheService.kt @@ -0,0 +1,29 @@ +package net.xintanalabs.rssotto.service + +import net.xintanalabs.rssotto.model.SiteCache +import net.xintanalabs.rssotto.repository.SiteCacheRepository +import org.springframework.stereotype.Service + +@Service +class SiteCacheService( + private val siteCacheRepository: SiteCacheRepository +) { + fun clearCache() { + siteCacheRepository.deleteAll() + } + + fun getCacheByDeveloperId(developerId: String): SiteCache? = + siteCacheRepository.getByDeveloperId(developerId) + + fun create(siteCache: SiteCache): SiteCache = + siteCacheRepository.create(siteCache) + + fun isCacheValid(siteCache: SiteCache): Boolean { + val currentTime = System.currentTimeMillis() + return currentTime < siteCache.expiresAt + } + + fun deleteById(id: String) { + siteCacheRepository.deleteById(id) + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/service/TypeService.kt b/src/main/kotlin/net/xintanalabs/rssotto/service/TypeService.kt index b1562e9..4c80483 100644 --- a/src/main/kotlin/net/xintanalabs/rssotto/service/TypeService.kt +++ b/src/main/kotlin/net/xintanalabs/rssotto/service/TypeService.kt @@ -9,14 +9,14 @@ class TypeService( private val typeRepository: TypeRepository ) { fun create(type: Type): Type? { - return typeRepository.createType(type) + return typeRepository.create(type) } fun findAll(): List { - return typeRepository.getAllTypes() + return typeRepository.getAll() } fun findById(id: String): Type? { - return typeRepository.getTypeById(id) + return typeRepository.getById(id) } } \ No newline at end of file diff --git a/src/main/kotlin/net/xintanalabs/rssotto/tasks/ScheduledTasks.kt b/src/main/kotlin/net/xintanalabs/rssotto/tasks/ScheduledTasks.kt index 96fa197..ff84115 100644 --- a/src/main/kotlin/net/xintanalabs/rssotto/tasks/ScheduledTasks.kt +++ b/src/main/kotlin/net/xintanalabs/rssotto/tasks/ScheduledTasks.kt @@ -1,5 +1,7 @@ package net.xintanalabs.rssotto.tasks +import net.xintanalabs.rssotto.config.RssottoProperties +import net.xintanalabs.rssotto.service.ActionsService import org.slf4j.LoggerFactory import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Component @@ -7,12 +9,18 @@ import java.text.SimpleDateFormat import java.util.Date @Component -class ScheduledTasks { +class ScheduledTasks( + private val actionsService: ActionsService +) { private val log = LoggerFactory.getLogger(ScheduledTasks::class.java) private val dateFormat = SimpleDateFormat("HH:mm:ss") - @Scheduled(fixedRate = 500000) - fun reportCurrentTime() { - log.info("The time is now {}", dateFormat.format(Date())) + //@Scheduled(cron = "\${rssotto.check-task-cron}") + @Scheduled(fixedDelayString = "\${rssotto.check-task-fixed-delay}", initialDelayString = "\${rssotto.check-task-initial-delay}") + + fun checkApps() { + log.info("Check task started at {}", dateFormat.format(Date())) + actionsService.checkAllVersions() + log.info("Check task completed at {}", dateFormat.format(Date())) } } \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 5ba63c2..183affc 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -25,7 +25,9 @@ logging: mongodb: DEBUG security: DEBUG -version-checker: - interval-minutes: 5 +rssotto: + check-task-cron: "0 */30 * * * *" + check-task-initial-delay: 10000 + check-task-fixed-delay: 21600000 diff --git a/src/test/kotlin/net/xintanalabs/rssotto/RssottoApplicationTests.kt b/src/test/kotlin/net/xintanalabs/rssotto/RssottoApplicationTests.kt index 228c797..ea53d19 100644 --- a/src/test/kotlin/net/xintanalabs/rssotto/RssottoApplicationTests.kt +++ b/src/test/kotlin/net/xintanalabs/rssotto/RssottoApplicationTests.kt @@ -3,11 +3,12 @@ package net.xintanalabs.rssotto import org.junit.jupiter.api.Test import org.springframework.boot.test.context.SpringBootTest -@SpringBootTest +//SpringBootTest class RssottoApplicationTests { - @Test + //Test fun contextLoads() { + } }