Update
This commit is contained in:
33
Dockerfile
Normal file
33
Dockerfile
Normal 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"]
|
||||
@@ -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")
|
||||
|
||||
@@ -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<String>) {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package net.xintanalabs.rssotto.components.checkers
|
||||
|
||||
import net.xintanalabs.rssotto.model.Source
|
||||
|
||||
interface IVersionChecker {
|
||||
suspend fun getLatestVersion(paramsDict: Map<String, String>): String
|
||||
suspend fun getLatestVersion(paramsDict: Map<String, String>): String?
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import kotlin.text.get
|
||||
|
||||
@Component
|
||||
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() }
|
||||
?: throw IllegalArgumentException("API URL required")
|
||||
val jsonPath = paramsDict["jsonPath"]?.takeIf { it.isNotEmpty() }
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,7 @@
|
||||
package net.xintanalabs.rssotto.components.checker.exceptions
|
||||
|
||||
class ScraperFetcherException(
|
||||
message: String,
|
||||
status: Int? = null,
|
||||
cause: Throwable? = null
|
||||
) : Exception(message, cause)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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, String>): 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, String>): String? {
|
||||
val info = mutableMapOf<String, String>()
|
||||
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("<script.*?</script>"), "")
|
||||
.replace(Regex("<style.*?</style>"), "")
|
||||
.replace(Regex("<!--.*?-->"), "")
|
||||
.replace(Regex("\\s+"), " ")
|
||||
.trim()
|
||||
}
|
||||
}
|
||||
@@ -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 * * * *"
|
||||
)
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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<String>): List<AppVersionResponse> {
|
||||
return appService.getAppsByVersions(version).map { it.toVersionResponse() }
|
||||
}
|
||||
|
||||
@GetMapping("/search")
|
||||
fun searchApps(@RequestParam q: String): List<AppResponse> {
|
||||
return appService.searchApps(q).map { it.toResponse() }
|
||||
fun searchApps(@RequestParam q: String?, @RequestParam id: List<String>?): List<AppResponse> {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
@@ -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>
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package net.xintanalabs.rssotto.controller.developer
|
||||
|
||||
data class DeveloperRequest(
|
||||
val name: String,
|
||||
val legalName: String,
|
||||
val webUrl: String
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<T: Any> (
|
||||
@@ -10,27 +9,31 @@ abstract class MongoDBAbstract<T: Any> (
|
||||
protected abstract val entityClass: Class<T>
|
||||
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<T> {
|
||||
protected fun getAllEntities(): List<T> {
|
||||
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<String, Any>): Long {
|
||||
protected fun updateEntity(id: String, updateFields: Map<String, Any>): Long {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,11 @@ class MongoDBClient(private val mongoTemplate: MongoTemplate) {
|
||||
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(
|
||||
collectionName: String,
|
||||
aggregation: Aggregation,
|
||||
|
||||
5
src/main/kotlin/net/xintanalabs/rssotto/enums/ModType.kt
Normal file
5
src/main/kotlin/net/xintanalabs/rssotto/enums/ModType.kt
Normal file
@@ -0,0 +1,5 @@
|
||||
package net.xintanalabs.rssotto.enums
|
||||
|
||||
enum class ModType {
|
||||
AIRCRAFT, AIRPORT, SCENERY, DESKTOP, UTILITY, OTHER, NOT_SET
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package net.xintanalabs.rssotto.enums
|
||||
|
||||
enum class NotificationType {
|
||||
VERSION_MISMATCH,
|
||||
TIMEOUT,
|
||||
UNREACHABLE,
|
||||
CHECK_FAILED,
|
||||
ONE_TIME_TASK_MISCONFIG
|
||||
}
|
||||
5
src/main/kotlin/net/xintanalabs/rssotto/enums/Role.kt
Normal file
5
src/main/kotlin/net/xintanalabs/rssotto/enums/Role.kt
Normal file
@@ -0,0 +1,5 @@
|
||||
package net.xintanalabs.rssotto.enums
|
||||
|
||||
enum class Role {
|
||||
USER, ADMIN
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package net.xintanalabs.rssotto.enums
|
||||
|
||||
enum class Severity {
|
||||
LOW, MEDIUM, HIGH, CRITICAL
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
11
src/main/kotlin/net/xintanalabs/rssotto/model/CheckerType.kt
Normal file
11
src/main/kotlin/net/xintanalabs/rssotto/model/CheckerType.kt
Normal 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>
|
||||
)
|
||||
10
src/main/kotlin/net/xintanalabs/rssotto/model/Developer.kt
Normal file
10
src/main/kotlin/net/xintanalabs/rssotto/model/Developer.kt
Normal 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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
11
src/main/kotlin/net/xintanalabs/rssotto/model/SiteCache.kt
Normal file
11
src/main/kotlin/net/xintanalabs/rssotto/model/SiteCache.kt
Normal 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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
@@ -15,26 +15,26 @@ class AppRepository(
|
||||
override val entityClass: Class<App> = App::class.java
|
||||
|
||||
fun createApp(app: App): App {
|
||||
return create(app)
|
||||
return createEntity(app)
|
||||
}
|
||||
|
||||
fun getAllApps(): List<App> {
|
||||
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<String, Any>): Long {
|
||||
return update(id, updateFields)
|
||||
return updateEntity(id, updateFields)
|
||||
}
|
||||
|
||||
fun findAppsByCriteria(criteria: Criteria): List<App> {
|
||||
return findByCriteria(criteria)
|
||||
return findEntitiesByCriteria(criteria)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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> = Source::class.java
|
||||
|
||||
fun createSource(source: Source): Source {
|
||||
return create(source)
|
||||
return createEntity(source)
|
||||
}
|
||||
|
||||
fun getAllSources(): List<Source> {
|
||||
return getAll()
|
||||
return getAllEntities()
|
||||
}
|
||||
|
||||
fun getSourceById(id: String): Source? {
|
||||
return getById(id)
|
||||
return getEntityById(id)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,15 +13,15 @@ class TypeRepository(
|
||||
override val collection: String = Constants.COLLECTION_TYPES
|
||||
override val entityClass: Class<Type> = Type::class.java
|
||||
|
||||
fun createType(type: Type): Type {
|
||||
return create(type)
|
||||
fun create(type: Type): Type {
|
||||
return createEntity(type)
|
||||
}
|
||||
|
||||
fun getAllTypes(): List<Type> {
|
||||
return getAll()
|
||||
fun getAll(): List<Type> {
|
||||
return getAllEntities()
|
||||
}
|
||||
|
||||
fun getTypeById(id: String): Type? {
|
||||
return getById(id)
|
||||
fun getById(id: String): Type? {
|
||||
return getEntityById(id)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 ?: ""
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,16 @@ class AppService(
|
||||
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 {
|
||||
return appRepository.deleteApp(id)
|
||||
}
|
||||
@@ -39,17 +49,13 @@ class AppService(
|
||||
}
|
||||
|
||||
fun updateLatestVersion(appId: String, latestVersion: String): Long {
|
||||
val updateFields = mapOf<String, Any>(
|
||||
"latestVersion" to latestVersion
|
||||
val updateFields = mapOf(
|
||||
"latestVersion" to latestVersion,
|
||||
"lastCheckedAt" to System.currentTimeMillis()
|
||||
)
|
||||
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> {
|
||||
val regex = ".*${Regex.escape(query)}.*"
|
||||
val criteria = Criteria.where("name").regex(regex, "i")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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<Type> {
|
||||
return typeRepository.getAllTypes()
|
||||
return typeRepository.getAll()
|
||||
}
|
||||
|
||||
fun findById(id: String): Type? {
|
||||
return typeRepository.getTypeById(id)
|
||||
return typeRepository.getById(id)
|
||||
}
|
||||
}
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user