This commit is contained in:
Jose Conde
2025-10-24 16:56:04 +02:00
parent 36d8a1dd32
commit 760589a0ee
57 changed files with 939 additions and 97 deletions

33
Dockerfile Normal file
View File

@@ -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"]

View File

@@ -43,6 +43,10 @@ dependencies {
implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.seleniumhq.selenium:selenium-java:4.35.0") 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.security:spring-security-test")
testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")

View File

@@ -2,8 +2,10 @@ package net.xintanalabs.rssotto
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication import org.springframework.boot.runApplication
import org.springframework.scheduling.annotation.EnableScheduling
@SpringBootApplication @SpringBootApplication
@EnableScheduling
class RssottoApplication class RssottoApplication
fun main(args: Array<String>) { fun main(args: Array<String>) {

View File

@@ -1,19 +1,22 @@
package net.xintanalabs.rssotto.components.checkers package net.xintanalabs.rssotto.components.checkers
import net.xintanalabs.rssotto.components.checker.api.ApiChecker 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 net.xintanalabs.rssotto.components.checkers.scrape.ScrapeChecker
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
@Component @Component
class CheckerFactory( class CheckerFactory(
private val apiChecker: ApiChecker, private val apiChecker: ApiChecker,
private val scrapeChecker: ScrapeChecker private val scrapeChecker: ScrapeChecker,
private val manualChecker: ManualChecker
) { ) {
fun createChecker(type: String): IVersionChecker { fun createChecker(type: String): IVersionChecker {
val parts = type.split(":") val parts = type.split(":")
return when (parts[0].lowercase()) { return when (parts[0].lowercase()) {
"scrape" -> scrapeChecker "scrape" -> scrapeChecker
"api" -> apiChecker "api" -> apiChecker
"manual" -> manualChecker
else -> throw IllegalArgumentException("Unknown checker type: $type") else -> throw IllegalArgumentException("Unknown checker type: $type")
} }
} }

View File

@@ -1,7 +1,5 @@
package net.xintanalabs.rssotto.components.checkers package net.xintanalabs.rssotto.components.checkers
import net.xintanalabs.rssotto.model.Source
interface IVersionChecker { interface IVersionChecker {
suspend fun getLatestVersion(paramsDict: Map<String, String>): String suspend fun getLatestVersion(paramsDict: Map<String, String>): String?
} }

View File

@@ -10,7 +10,7 @@ import kotlin.text.get
@Component @Component
class ApiChecker(private val webClient: WebClient) : IVersionChecker { class ApiChecker(private val webClient: WebClient) : IVersionChecker {
override suspend fun getLatestVersion(paramsDict: Map<String, String>): String { override suspend fun getLatestVersion(paramsDict: Map<String, String>): String? {
val url = paramsDict["url"]?.takeIf { it.isNotEmpty() } val url = paramsDict["url"]?.takeIf { it.isNotEmpty() }
?: throw IllegalArgumentException("API URL required") ?: throw IllegalArgumentException("API URL required")
val jsonPath = paramsDict["jsonPath"]?.takeIf { it.isNotEmpty() } val jsonPath = paramsDict["jsonPath"]?.takeIf { it.isNotEmpty() }

View File

@@ -0,0 +1,7 @@
package net.xintanalabs.rssotto.components.checker.exceptions
class CheckerException(
message: String?,
val info: Map<String, String>? = null,
cause: Throwable? = null,
) : Exception(message, cause)

View File

@@ -0,0 +1,7 @@
package net.xintanalabs.rssotto.components.checker.exceptions
class ScraperFetcherException(
message: String,
status: Int? = null,
cause: Throwable? = null
) : Exception(message, cause)

View File

@@ -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, String>): String? {
val id = paramsDict["id"] ?: return ""
val app = appService.getAppById(id)
return app?.latestVersion
}
}

View File

@@ -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()
}
}
}

View File

@@ -1,6 +1,8 @@
package net.xintanalabs.rssotto.components.checkers.scrape package net.xintanalabs.rssotto.components.checkers.scrape
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import net.xintanalabs.rssotto.components.checker.exceptions.ScraperFetcherException
import org.jsoup.HttpStatusException
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
@@ -18,6 +20,8 @@ class JSoupFetcher: IScrapeFetcher {
.header("Upgrade-Insecure-Requests", "1") .header("Upgrade-Insecure-Requests", "1")
.get() .get()
.html() .html()
} catch (httpe: HttpStatusException) {
throw ScraperFetcherException("HTTP error fetching ${url}: ${httpe.statusCode}", httpe.statusCode, httpe)
} catch (e: Exception) { } catch (e: Exception) {
throw RuntimeException("Error fetching ${url}", e) throw RuntimeException("Error fetching ${url}", e)
} }

View File

@@ -1,9 +1,10 @@
package net.xintanalabs.rssotto.components.checkers.scrape 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.components.checkers.IVersionChecker
import net.xintanalabs.rssotto.model.Source import net.xintanalabs.rssotto.model.SiteCache
import net.xintanalabs.rssotto.tasks.ScheduledTasks import net.xintanalabs.rssotto.service.SiteCacheService
import org.openqa.selenium.chrome.ChromeDriver import org.openqa.selenium.chrome.ChromeDriver
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
@@ -13,31 +14,82 @@ import java.util.regex.Pattern
@Component @Component
class ScrapeChecker( class ScrapeChecker(
private val restTemplate: RestTemplate, private val restTemplate: RestTemplate,
private val chromeDriver: ChromeDriver private val chromeDriver: ChromeDriver,
private val siteCacheService: SiteCacheService
) : IVersionChecker { ) : IVersionChecker {
private val log = LoggerFactory.getLogger(ScrapeChecker::class.java) private val log = LoggerFactory.getLogger(ScrapeChecker::class.java)
override suspend fun getLatestVersion(paramsDict: Map<String, String>): String { override suspend fun getLatestVersion(paramsDict: Map<String, String>): String? {
val url = paramsDict["url"]?.takeIf { it.isNotEmpty() } val info = mutableMapOf<String, String>()
?: throw IllegalArgumentException("URL required") return try {
log.info("Url : {}", url) val url = paramsDict["url"]?.takeIf { it.isNotEmpty() }
val mode = paramsDict["mode"] ?: throw IllegalArgumentException("URL required")
log.info("Mode : {}", mode) info["url"] = url
val fetcher: IScrapeFetcher = when (mode) { val mode = paramsDict["mode"]
"selenium" -> SeleniumFetcher(chromeDriver) val cached: Boolean = paramsDict["cached"]?.toBoolean() ?: false
"jsoup" -> JSoupFetcher() info["mode"] = mode ?: ""
else -> DefaultScrapeFetcher(restTemplate)
}
val response = fetcher.fetch(url)
val cleanedResponse = response.replace(">\\s+<".toRegex(), "><")
val regex: String = paramsDict["regex"] as String val fetcher: IScrapeFetcher = when (mode) {
log.info("Regex : {}", regex) "selenium" -> SeleniumFetcher(chromeDriver)
val match = Pattern.compile(regex).matcher(cleanedResponse) "jsoup" -> JSoupFetcher()
if (!match.find() || match.groupCount() < 1) { "flaresolverr" -> FlaresSolverrFetcher()
throw Exception("No match with regex in response") 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("<script.*?</script>"), "")
.replace(Regex("<style.*?</style>"), "")
.replace(Regex("<!--.*?-->"), "")
.replace(Regex("\\s+"), " ")
.trim()
} }
} }

View File

@@ -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 * * * *"
)

View File

@@ -9,12 +9,27 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
import org.springframework.security.config.http.SessionCreationPolicy import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.web.DefaultSecurityFilterChain import org.springframework.security.web.DefaultSecurityFilterChain
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter 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 @Configuration
@EnableWebSecurity @EnableWebSecurity
class SecurityConfiguration( class SecurityConfiguration(
private val authenticationProvider: AuthenticationProvider 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 @Bean
fun securityFilterChain( fun securityFilterChain(
@@ -22,10 +37,11 @@ class SecurityConfiguration(
jwtAuthenticationFilter: JwtAuthenticationFilter jwtAuthenticationFilter: JwtAuthenticationFilter
): DefaultSecurityFilterChain = ): DefaultSecurityFilterChain =
http http
.cors { }
.csrf { it.disable() } .csrf { it.disable() }
.authorizeHttpRequests { .authorizeHttpRequests {
it it
.requestMatchers("/api/auth", "/api/auth/refresh", "/error") .requestMatchers("/api/auth/**", "/error")
.permitAll() .permitAll()
.requestMatchers(HttpMethod.POST, "/api/user") .requestMatchers(HttpMethod.POST, "/api/user")
.permitAll() .permitAll()
@@ -40,4 +56,5 @@ class SecurityConfiguration(
.authenticationProvider(authenticationProvider) .authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java)
.build() .build()
} }

View File

@@ -6,4 +6,7 @@ object Constants {
const val COLLECTION_APPS: String = "apps" const val COLLECTION_APPS: String = "apps"
const val COLLECTION_SOURCES: String = "sources" const val COLLECTION_SOURCES: String = "sources"
const val COLLECTION_CHECKER_TYPES: String = "checkerTypes" 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"
} }

View File

@@ -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<String>): List<AppVersionResponse> {
return actionsService.checkVersions(idList)
}
@PostMapping("/check-all")
fun checkAllApps(): List<AppVersionResponse> {
return actionsService.checkAllVersions()
}
}

View File

@@ -1,6 +1,7 @@
package net.xintanalabs.rssotto.controller.app package net.xintanalabs.rssotto.controller.app
import net.xintanalabs.rssotto.constants.Constants import net.xintanalabs.rssotto.constants.Constants
import net.xintanalabs.rssotto.enums.ModType
import net.xintanalabs.rssotto.model.App import net.xintanalabs.rssotto.model.App
import net.xintanalabs.rssotto.service.AppService import net.xintanalabs.rssotto.service.AppService
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
@@ -36,14 +37,15 @@ class AppController(
appService.getAllApps() appService.getAllApps()
.map { it.toResponse() } .map { it.toResponse() }
@GetMapping("/versions")
fun getAppsVersions(@RequestParam version: List<String>): List<AppVersionResponse> {
return appService.getAppsByVersions(version).map { it.toVersionResponse() }
}
@GetMapping("/search") @GetMapping("/search")
fun searchApps(@RequestParam q: String): List<AppResponse> { fun searchApps(@RequestParam q: String?, @RequestParam id: List<String>?): List<AppResponse> {
return appService.searchApps(q).map { it.toResponse() } 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 { private fun App.toVersionResponse(): AppVersionResponse {
@@ -56,7 +58,6 @@ class AppController(
private fun App.toResponse(): AppResponse { private fun App.toResponse(): AppResponse {
return AppResponse( return AppResponse(
id = this.id, id = this.id,
uid = this.uid,
name = this.name, name = this.name,
type = this.type, type = this.type,
source = this.source, source = this.source,
@@ -67,13 +68,14 @@ class AppController(
createdAt = this.createdAt, createdAt = this.createdAt,
updatedAt = this.updatedAt, updatedAt = this.updatedAt,
lastCheckedAt = this.lastCheckedAt, lastCheckedAt = this.lastCheckedAt,
active = this.active active = this.active,
developer = this.developer,
modType = this.modType ?: ModType.NOT_SET
) )
} }
private fun AppRequest.toModel(): App { private fun AppRequest.toModel(): App {
return App( return App(
uid = this.uid,
name = this.name, name = this.name,
type = this.type, type = this.type,
source = this.source, source = this.source,
@@ -84,7 +86,9 @@ class AppController(
createdAt = System.currentTimeMillis(), createdAt = System.currentTimeMillis(),
updatedAt = System.currentTimeMillis(), updatedAt = System.currentTimeMillis(),
lastCheckedAt = 0, lastCheckedAt = 0,
active = true active = true,
developer = this.developer,
modType = this.modType
) )
} }
} }

View File

@@ -1,7 +1,8 @@
package net.xintanalabs.rssotto.controller.app package net.xintanalabs.rssotto.controller.app
import net.xintanalabs.rssotto.enums.ModType
data class AppRequest( data class AppRequest(
val uid: String,
val name: String, val name: String,
val type: String = "", val type: String = "",
val source: String, val source: String,
@@ -12,5 +13,7 @@ data class AppRequest(
val createdAt: Long = 0, val createdAt: Long = 0,
val updatedAt: Long = 0, val updatedAt: Long = 0,
val lastCheckedAt: Long = 0, val lastCheckedAt: Long = 0,
val active: Boolean = true val active: Boolean = true,
val developer: String? = null,
val modType: ModType
) )

View File

@@ -1,8 +1,9 @@
package net.xintanalabs.rssotto.controller.app package net.xintanalabs.rssotto.controller.app
import net.xintanalabs.rssotto.enums.ModType
data class AppResponse( data class AppResponse(
val id: String? = null, val id: String? = null,
val uid: String,
val name: String, val name: String,
val type: String = "", val type: String = "",
val source: String, val source: String,
@@ -13,5 +14,7 @@ data class AppResponse(
val createdAt: Long = 0, val createdAt: Long = 0,
val updatedAt: Long = 0, val updatedAt: Long = 0,
val lastCheckedAt: Long = 0, val lastCheckedAt: Long = 0,
val active: Boolean = true val active: Boolean = true,
val developer: String? = null,
val modType: ModType
) )

View File

@@ -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<CheckerTypeResponse> {
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
)
}

View File

@@ -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<Field>
)

View File

@@ -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<Field>
)

View File

@@ -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<DeveloperResponse> {
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,
)
}

View File

@@ -0,0 +1,7 @@
package net.xintanalabs.rssotto.controller.developer
data class DeveloperRequest(
val name: String,
val legalName: String,
val webUrl: String
)

View File

@@ -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
)

View File

@@ -15,7 +15,7 @@ import org.springframework.web.server.ResponseStatusException
@RequestMapping("${Constants.API_BASE_PATH}/source") @RequestMapping("${Constants.API_BASE_PATH}/source")
class SourceController(private val sourceService: SourceService) { class SourceController(private val sourceService: SourceService) {
@PostMapping @PostMapping
suspend fun create(@RequestBody sourceRequest: SourceRequest): SourceResponse { fun create(@RequestBody sourceRequest: SourceRequest): SourceResponse {
return try { return try {
sourceService.create(sourceRequest.toModel()) sourceService.create(sourceRequest.toModel())
.toResponse() .toResponse()

View File

@@ -1,6 +1,6 @@
package net.xintanalabs.rssotto.controller.user 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.model.User
import net.xintanalabs.rssotto.service.UserService import net.xintanalabs.rssotto.service.UserService
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus

View File

@@ -1,6 +1,5 @@
package net.xintanalabs.rssotto.db.mongodb package net.xintanalabs.rssotto.db.mongodb
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.mongodb.core.query.Criteria import org.springframework.data.mongodb.core.query.Criteria
abstract class MongoDBAbstract<T: Any> ( abstract class MongoDBAbstract<T: Any> (
@@ -10,27 +9,31 @@ abstract class MongoDBAbstract<T: Any> (
protected abstract val entityClass: Class<T> protected abstract val entityClass: Class<T>
protected val idField: String = "id" protected val idField: String = "id"
protected fun create(entity: T): T { protected fun createEntity(entity: T): T {
return mongoDBClient.insert(collection, entity, entityClass) return mongoDBClient.insert(collection, entity, entityClass)
} }
protected fun getAll(): List<T> { protected fun getAllEntities(): List<T> {
return mongoDBClient.findAll(collection, entityClass) 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) 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) return mongoDBClient.deleteOne(collection, idField, id, entityClass)
} }
protected fun update(id: String, updateFields: Map<String, Any>): Long { protected fun updateEntity(id: String, updateFields: Map<String, Any>): Long {
return mongoDBClient.updateOne(collection, idField, id, updateFields, entityClass) return mongoDBClient.updateOne(collection, idField, id, updateFields, entityClass)
} }
protected fun findByCriteria(criteria: Criteria): List<T> { protected fun findEntitiesByCriteria(criteria: Criteria): List<T> {
return mongoDBClient.findByFilter(collection, criteria, entityClass) return mongoDBClient.findByFilter(collection, criteria, entityClass)
} }
} }

View File

@@ -54,6 +54,11 @@ class MongoDBClient(private val mongoTemplate: MongoTemplate) {
return mongoTemplate.remove(query, clazz, collectionName).deletedCount return mongoTemplate.remove(query, clazz, collectionName).deletedCount
} }
fun <T: Any> deleteMany(collectionName: String, criteria: Criteria, clazz: Class<T>): Long {
val query = Query(criteria)
return mongoTemplate.remove(query, clazz, collectionName).deletedCount
}
fun <T: Any, R: Any> aggregate( fun <T: Any, R: Any> aggregate(
collectionName: String, collectionName: String,
aggregation: Aggregation, aggregation: Aggregation,

View File

@@ -0,0 +1,5 @@
package net.xintanalabs.rssotto.enums
enum class ModType {
AIRCRAFT, AIRPORT, SCENERY, DESKTOP, UTILITY, OTHER, NOT_SET
}

View File

@@ -0,0 +1,9 @@
package net.xintanalabs.rssotto.enums
enum class NotificationType {
VERSION_MISMATCH,
TIMEOUT,
UNREACHABLE,
CHECK_FAILED,
ONE_TIME_TASK_MISCONFIG
}

View File

@@ -0,0 +1,5 @@
package net.xintanalabs.rssotto.enums
enum class Role {
USER, ADMIN
}

View File

@@ -0,0 +1,5 @@
package net.xintanalabs.rssotto.enums
enum class Severity {
LOW, MEDIUM, HIGH, CRITICAL
}

View File

@@ -1,13 +1,13 @@
package net.xintanalabs.rssotto.model package net.xintanalabs.rssotto.model
import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonInclude
import net.xintanalabs.rssotto.enums.ModType
import org.springframework.data.annotation.Id import org.springframework.data.annotation.Id
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
data class App( data class App(
@Id val id: String? = null, @Id val id: String? = null,
val uid: String,
val name: String, val name: String,
val type: String = "", val type: String = "",
val source: String, val source: String,
@@ -18,5 +18,7 @@ data class App(
val createdAt: Long = 0, val createdAt: Long = 0,
val updatedAt: Long = 0, val updatedAt: Long = 0,
val lastCheckedAt: Long = 0, val lastCheckedAt: Long = 0,
val active: Boolean = true val active: Boolean = true,
val developer: String? = null,
val modType: ModType? = null
) )

View File

@@ -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<Field>
)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -1,5 +1,6 @@
package net.xintanalabs.rssotto.model package net.xintanalabs.rssotto.model
import net.xintanalabs.rssotto.enums.Role
import java.util.UUID import java.util.UUID
data class User( data class User(
@@ -7,9 +8,4 @@ data class User(
val username: String, val username: String,
val password: String, val password: String,
val role: Role val role: Role
) )
enum class Role {
USER, ADMIN
}

View File

@@ -15,26 +15,26 @@ class AppRepository(
override val entityClass: Class<App> = App::class.java override val entityClass: Class<App> = App::class.java
fun createApp(app: App): App { fun createApp(app: App): App {
return create(app) return createEntity(app)
} }
fun getAllApps(): List<App> { fun getAllApps(): List<App> {
return getAll() return getAllEntities()
} }
fun getAppById(id: String): App? { fun getAppById(id: String): App? {
return getById(id) return getEntityById(id)
} }
fun deleteApp(id: String): Long { fun deleteApp(id: String): Long {
return delete(id) return deleteEntity(id)
} }
fun updateApp(id: String, updateFields: Map<String, Any>): Long { fun updateApp(id: String, updateFields: Map<String, Any>): Long {
return update(id, updateFields) return updateEntity(id, updateFields)
} }
fun findAppsByCriteria(criteria: Criteria): List<App> { fun findAppsByCriteria(criteria: Criteria): List<App> {
return findByCriteria(criteria) return findEntitiesByCriteria(criteria)
} }
} }

View File

@@ -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<CheckerType>(mongoDBClient) {
override val collection: String = Constants.COLLECTION_CHECKER_TYPES
override val entityClass: Class<CheckerType> = CheckerType::class.java
fun createCheckerType(checkerType: CheckerType): CheckerType {
return createEntity(checkerType)
}
fun getAllCheckerTypes(): List<CheckerType> {
return try {
getAllEntities()
} catch (e: Exception) {
println("Error fetching all checker types: ${e.message}")
throw e
}
}
fun getCheckerTypeById(id: String): CheckerType? {
return getEntityById(id)
}
}

View File

@@ -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<Developer>(mongoDBClient) {
override val collection: String = Constants.COLLECTION_DEVELOPERS
override val entityClass: Class<Developer> = Developer::class.java
fun create(developer: Developer): Developer {
return createEntity(developer)
}
fun getAll(): List<Developer> {
return getAllEntities()
}
fun getById(id: String): Developer? {
return getEntityById(id)
}
}

View File

@@ -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<Notification>(mongoDBClient) {
override val collection: String = Constants.COLLECTION_NOTIFICATIONS
override val entityClass: Class<Notification> = Notification::class.java
fun createNotification(notification: Notification): Notification {
return createEntity(notification)
}
fun getAllNotifications(): List<Notification> {
return getAllEntities()
}
fun getNotificationById(id: String): Notification? {
return getEntityById(id)
}
fun getNotificationByFilter(criteria: Criteria): List<Notification> {
return findEntitiesByCriteria(criteria)
}
}

View File

@@ -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<SiteCache>(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)
}
}

View File

@@ -4,7 +4,6 @@ import net.xintanalabs.rssotto.constants.Constants
import net.xintanalabs.rssotto.db.mongodb.MongoDBAbstract import net.xintanalabs.rssotto.db.mongodb.MongoDBAbstract
import net.xintanalabs.rssotto.db.mongodb.MongoDBClient import net.xintanalabs.rssotto.db.mongodb.MongoDBClient
import net.xintanalabs.rssotto.model.Source import net.xintanalabs.rssotto.model.Source
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
@Repository @Repository
@@ -15,15 +14,15 @@ class SourceRepository (
override val entityClass: Class<Source> = Source::class.java override val entityClass: Class<Source> = Source::class.java
fun createSource(source: Source): Source { fun createSource(source: Source): Source {
return create(source) return createEntity(source)
} }
fun getAllSources(): List<Source> { fun getAllSources(): List<Source> {
return getAll() return getAllEntities()
} }
fun getSourceById(id: String): Source? { fun getSourceById(id: String): Source? {
return getById(id) return getEntityById(id)
} }
} }

View File

@@ -13,15 +13,15 @@ class TypeRepository(
override val collection: String = Constants.COLLECTION_TYPES override val collection: String = Constants.COLLECTION_TYPES
override val entityClass: Class<Type> = Type::class.java override val entityClass: Class<Type> = Type::class.java
fun createType(type: Type): Type { fun create(type: Type): Type {
return create(type) return createEntity(type)
} }
fun getAllTypes(): List<Type> { fun getAll(): List<Type> {
return getAll() return getAllEntities()
} }
fun getTypeById(id: String): Type? { fun getById(id: String): Type? {
return getById(id) return getEntityById(id)
} }
} }

View File

@@ -1,6 +1,6 @@
package net.xintanalabs.rssotto.repository package net.xintanalabs.rssotto.repository
import net.xintanalabs.rssotto.model.Role import net.xintanalabs.rssotto.enums.Role
import net.xintanalabs.rssotto.model.User import net.xintanalabs.rssotto.model.User
import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository

View File

@@ -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<String>): List<AppVersionResponse> {
val apps = appService.getAppsByIds(idList)
return checkApps(apps)
}
fun checkAllVersions(): List<AppVersionResponse> {
val apps = appService.getAllApps()
return checkApps(apps)
}
private fun checkApps(apps: List<App>): List<AppVersionResponse> {
val sources = sourceService.findALl()
val checkerTypes = checkerTypeService.findAll()
val versionList = mutableListOf<AppVersionResponse>()
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<Source>,
checkerTypes: List<CheckerType>
): 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<String, String> {
val paramsMap = mutableMapOf<String, String>()
checkerType.params.forEach { field ->
val value = getValue(field.name, app, source)
paramsMap[field.name] = value
}
return additionalParams(paramsMap, app)
}
private fun additionalParams(map: MutableMap<String, String>, app: App): Map<String, String> {
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 ?: ""
}
}

View File

@@ -23,6 +23,16 @@ class AppService(
return appRepository.getAppById(id) return appRepository.getAppById(id)
} }
fun getAppsByIds(ids: List<String>): List<App> {
val criteria = Criteria.where("_id").`in`(ids)
return appRepository.findAppsByCriteria(criteria)
}
fun getAppsByVersions(versions: List<String>): List<App> {
val criteria = Criteria.where("latestVersion").`in`(versions)
return appRepository.findAppsByCriteria(criteria)
}
fun deleteApp(id: String): Long { fun deleteApp(id: String): Long {
return appRepository.deleteApp(id) return appRepository.deleteApp(id)
} }
@@ -39,17 +49,13 @@ class AppService(
} }
fun updateLatestVersion(appId: String, latestVersion: String): Long { fun updateLatestVersion(appId: String, latestVersion: String): Long {
val updateFields = mapOf<String, Any>( val updateFields = mapOf(
"latestVersion" to latestVersion "latestVersion" to latestVersion,
"lastCheckedAt" to System.currentTimeMillis()
) )
return appRepository.updateApp(appId, updateFields) return appRepository.updateApp(appId, updateFields)
} }
fun getAppsByVersions(versions: List<String>): List<App> {
val criteria = Criteria.where("latestVersion").`in`(versions)
return appRepository.findAppsByCriteria(criteria)
}
fun searchApps(query: String): List<App> { fun searchApps(query: String): List<App> {
val regex = ".*${Regex.escape(query)}.*" val regex = ".*${Regex.escape(query)}.*"
val criteria = Criteria.where("name").regex(regex, "i") val criteria = Criteria.where("name").regex(regex, "i")

View File

@@ -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<CheckerType> {
return checkerTypeRepository.getAllCheckerTypes()
}
fun findById(id: String): CheckerType? {
return checkerTypeRepository.getCheckerTypeById(id)
}
fun create(checkerType: CheckerType): CheckerType? {
return checkerTypeRepository.createCheckerType(checkerType)
}
}

View File

@@ -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<Developer> {
return developerRepository.getAll()
}
fun getById(id: String): Developer? {
return developerRepository.getById(id)
}
fun create(developer: Developer): Developer {
return developerRepository.create(developer)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -9,14 +9,14 @@ class TypeService(
private val typeRepository: TypeRepository private val typeRepository: TypeRepository
) { ) {
fun create(type: Type): Type? { fun create(type: Type): Type? {
return typeRepository.createType(type) return typeRepository.create(type)
} }
fun findAll(): List<Type> { fun findAll(): List<Type> {
return typeRepository.getAllTypes() return typeRepository.getAll()
} }
fun findById(id: String): Type? { fun findById(id: String): Type? {
return typeRepository.getTypeById(id) return typeRepository.getById(id)
} }
} }

View File

@@ -1,5 +1,7 @@
package net.xintanalabs.rssotto.tasks package net.xintanalabs.rssotto.tasks
import net.xintanalabs.rssotto.config.RssottoProperties
import net.xintanalabs.rssotto.service.ActionsService
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.scheduling.annotation.Scheduled import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
@@ -7,12 +9,18 @@ import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
@Component @Component
class ScheduledTasks { class ScheduledTasks(
private val actionsService: ActionsService
) {
private val log = LoggerFactory.getLogger(ScheduledTasks::class.java) private val log = LoggerFactory.getLogger(ScheduledTasks::class.java)
private val dateFormat = SimpleDateFormat("HH:mm:ss") private val dateFormat = SimpleDateFormat("HH:mm:ss")
@Scheduled(fixedRate = 500000) //@Scheduled(cron = "\${rssotto.check-task-cron}")
fun reportCurrentTime() { @Scheduled(fixedDelayString = "\${rssotto.check-task-fixed-delay}", initialDelayString = "\${rssotto.check-task-initial-delay}")
log.info("The time is now {}", dateFormat.format(Date()))
fun checkApps() {
log.info("Check task started at {}", dateFormat.format(Date()))
actionsService.checkAllVersions()
log.info("Check task completed at {}", dateFormat.format(Date()))
} }
} }

View File

@@ -25,7 +25,9 @@ logging:
mongodb: DEBUG mongodb: DEBUG
security: DEBUG security: DEBUG
version-checker: rssotto:
interval-minutes: 5 check-task-cron: "0 */30 * * * *"
check-task-initial-delay: 10000
check-task-fixed-delay: 21600000

View File

@@ -3,11 +3,12 @@ package net.xintanalabs.rssotto
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest //SpringBootTest
class RssottoApplicationTests { class RssottoApplicationTests {
@Test //Test
fun contextLoads() { fun contextLoads() {
} }
} }