dev #1
							
								
								
									
										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()
 | 
			
		||||
@@ -41,3 +57,4 @@ class SecurityConfiguration(
 | 
			
		||||
            .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()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,94 @@
 | 
			
		||||
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
 | 
			
		||||
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.RequestParam
 | 
			
		||||
import org.springframework.web.bind.annotation.RestController
 | 
			
		||||
import org.springframework.web.server.ResponseStatusException
 | 
			
		||||
 | 
			
		||||
@RestController
 | 
			
		||||
@RequestMapping("${Constants.API_BASE_PATH}/app")
 | 
			
		||||
class AppController(
 | 
			
		||||
    private val appService: AppService
 | 
			
		||||
) {
 | 
			
		||||
    @PostMapping
 | 
			
		||||
    fun createApp(@RequestBody app: AppRequest): AppResponse? =
 | 
			
		||||
        appService.createApp(app.toModel())
 | 
			
		||||
            ?.toResponse()
 | 
			
		||||
            ?:  throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot create app")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @GetMapping("/{id}")
 | 
			
		||||
    fun getApp(@PathVariable id: String): AppResponse? =
 | 
			
		||||
         appService.getAppById(id)
 | 
			
		||||
            ?.toResponse()
 | 
			
		||||
            ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "App not found")
 | 
			
		||||
 | 
			
		||||
    @GetMapping
 | 
			
		||||
    fun  getAllApps(): List<AppResponse> =
 | 
			
		||||
        appService.getAllApps()
 | 
			
		||||
            .map { it.toResponse() }
 | 
			
		||||
 | 
			
		||||
    @GetMapping("/search")
 | 
			
		||||
    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 {
 | 
			
		||||
        return AppVersionResponse(
 | 
			
		||||
            id = this.id,
 | 
			
		||||
            latestVersion = this.latestVersion,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun App.toResponse(): AppResponse {
 | 
			
		||||
        return AppResponse(
 | 
			
		||||
            id = this.id,
 | 
			
		||||
            name = this.name,
 | 
			
		||||
            type = this.type,
 | 
			
		||||
            source = this.source,
 | 
			
		||||
            params = this.params,
 | 
			
		||||
            fields = this.fields,
 | 
			
		||||
            downloadUrl = this.downloadUrl,
 | 
			
		||||
            latestVersion = this.latestVersion,
 | 
			
		||||
            createdAt = this.createdAt,
 | 
			
		||||
            updatedAt = this.updatedAt,
 | 
			
		||||
            lastCheckedAt = this.lastCheckedAt,
 | 
			
		||||
            active = this.active,
 | 
			
		||||
            developer = this.developer,
 | 
			
		||||
            modType = this.modType ?: ModType.NOT_SET
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun AppRequest.toModel(): App {
 | 
			
		||||
        return App(
 | 
			
		||||
            name = this.name,
 | 
			
		||||
            type = this.type,
 | 
			
		||||
            source = this.source,
 | 
			
		||||
            params = this.params,
 | 
			
		||||
            fields = this.fields,
 | 
			
		||||
            downloadUrl =  this.downloadUrl,
 | 
			
		||||
            latestVersion = this.latestVersion,
 | 
			
		||||
            createdAt = System.currentTimeMillis(),
 | 
			
		||||
            updatedAt = System.currentTimeMillis(),
 | 
			
		||||
            lastCheckedAt = 0,
 | 
			
		||||
            active = true,
 | 
			
		||||
            developer = this.developer,
 | 
			
		||||
            modType = this.modType
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,19 @@
 | 
			
		||||
package net.xintanalabs.rssotto.controller.app
 | 
			
		||||
 | 
			
		||||
import net.xintanalabs.rssotto.enums.ModType
 | 
			
		||||
 | 
			
		||||
data class AppRequest(
 | 
			
		||||
    val name: String,
 | 
			
		||||
    val type: String = "",
 | 
			
		||||
    val source: String,
 | 
			
		||||
    val params: Map<String, String>,
 | 
			
		||||
    val fields: Map<String, String>,
 | 
			
		||||
    val downloadUrl: String,
 | 
			
		||||
    val latestVersion: String? = null,
 | 
			
		||||
    val createdAt: Long = 0,
 | 
			
		||||
    val updatedAt: Long = 0,
 | 
			
		||||
    val lastCheckedAt: Long = 0,
 | 
			
		||||
    val active: Boolean = true,
 | 
			
		||||
    val developer: String? = null,
 | 
			
		||||
    val modType: ModType
 | 
			
		||||
)
 | 
			
		||||
@@ -0,0 +1,20 @@
 | 
			
		||||
package net.xintanalabs.rssotto.controller.app
 | 
			
		||||
 | 
			
		||||
import net.xintanalabs.rssotto.enums.ModType
 | 
			
		||||
 | 
			
		||||
data class AppResponse(
 | 
			
		||||
    val id: String? = null,
 | 
			
		||||
    val name: String,
 | 
			
		||||
    val type: String = "",
 | 
			
		||||
    val source: String,
 | 
			
		||||
    val params: Map<String, String>,
 | 
			
		||||
    val fields: Map<String, String>,
 | 
			
		||||
    val downloadUrl: String,
 | 
			
		||||
    val latestVersion: String? = null,
 | 
			
		||||
    val createdAt: Long = 0,
 | 
			
		||||
    val updatedAt: Long = 0,
 | 
			
		||||
    val lastCheckedAt: Long = 0,
 | 
			
		||||
    val active: Boolean = true,
 | 
			
		||||
    val developer: String? = null,
 | 
			
		||||
    val modType: ModType
 | 
			
		||||
)
 | 
			
		||||
@@ -0,0 +1,8 @@
 | 
			
		||||
package net.xintanalabs.rssotto.controller.app
 | 
			
		||||
 | 
			
		||||
import org.springframework.data.annotation.Id
 | 
			
		||||
 | 
			
		||||
data class AppVersionResponse(
 | 
			
		||||
    @Id val id: String? = null,
 | 
			
		||||
    val latestVersion: String? = null
 | 
			
		||||
)
 | 
			
		||||
@@ -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()
 | 
			
		||||
@@ -25,9 +25,9 @@ class SourceController(private val sourceService: SourceService) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @GetMapping
 | 
			
		||||
    fun getAll(): List<Source> {
 | 
			
		||||
    fun getAll(): List<SourceResponse> {
 | 
			
		||||
        return try {
 | 
			
		||||
            sourceService.findALl()
 | 
			
		||||
            sourceService.findALl().map { it.toResponse() }
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
            throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Cannot retrieve sources: ${e.message}", e)
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,60 @@
 | 
			
		||||
package net.xintanalabs.rssotto.controller.type
 | 
			
		||||
 | 
			
		||||
import net.xintanalabs.rssotto.constants.Constants
 | 
			
		||||
import net.xintanalabs.rssotto.model.Type
 | 
			
		||||
import net.xintanalabs.rssotto.service.TypeService
 | 
			
		||||
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}/type")
 | 
			
		||||
class TypeController(
 | 
			
		||||
    private val typeService: TypeService
 | 
			
		||||
) {
 | 
			
		||||
    @PostMapping
 | 
			
		||||
    fun createType(@RequestBody type: TypeRequest): TypeResponse? {
 | 
			
		||||
        return typeService.create(type.toModel())
 | 
			
		||||
            ?.toResponse()
 | 
			
		||||
            ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot create user")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @GetMapping
 | 
			
		||||
    fun getAllTypes(): List<TypeResponse> {
 | 
			
		||||
        return typeService.findAll().map{ it.toResponse() }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @GetMapping("/{id}")
 | 
			
		||||
    fun getType(@PathVariable id: String): TypeResponse? {
 | 
			
		||||
        return typeService.findById(id)
 | 
			
		||||
            ?.toResponse()
 | 
			
		||||
            ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "User not found")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun TypeRequest.toModel(): Type =
 | 
			
		||||
        Type(
 | 
			
		||||
            id = null,
 | 
			
		||||
            shortName = this.shortName,
 | 
			
		||||
            name = this.name,
 | 
			
		||||
            configFields = this.configFields,
 | 
			
		||||
            appFields = this.appFields
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    private fun Type.toResponse(): TypeResponse =
 | 
			
		||||
        TypeResponse(
 | 
			
		||||
            id = this.id,
 | 
			
		||||
            shortName = this.shortName,
 | 
			
		||||
            name = this.name,
 | 
			
		||||
            configFields = this.configFields,
 | 
			
		||||
            appFields = this.appFields
 | 
			
		||||
        )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -0,0 +1,10 @@
 | 
			
		||||
package net.xintanalabs.rssotto.controller.type
 | 
			
		||||
 | 
			
		||||
import net.xintanalabs.rssotto.model.Field
 | 
			
		||||
 | 
			
		||||
data class TypeRequest(
 | 
			
		||||
    val shortName: String,
 | 
			
		||||
    val name: String,
 | 
			
		||||
    val configFields: List<Field>,
 | 
			
		||||
    val appFields: List<Field>
 | 
			
		||||
)
 | 
			
		||||
@@ -0,0 +1,11 @@
 | 
			
		||||
package net.xintanalabs.rssotto.controller.type
 | 
			
		||||
 | 
			
		||||
import net.xintanalabs.rssotto.model.Field
 | 
			
		||||
 | 
			
		||||
data class TypeResponse(
 | 
			
		||||
    val id: String? = null,
 | 
			
		||||
    val shortName: String,
 | 
			
		||||
    val name: String,
 | 
			
		||||
    val configFields: List<Field>,
 | 
			
		||||
    val appFields: List<Field>
 | 
			
		||||
)
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -26,8 +26,8 @@ class UserController(private val userService: UserService) {
 | 
			
		||||
            ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot create user")
 | 
			
		||||
 | 
			
		||||
    @GetMapping
 | 
			
		||||
    fun listAll(): List<UserResponse> = listOf()
 | 
			
		||||
        //userService.findAll().map { it.toResponse() }
 | 
			
		||||
    fun listAll(): List<UserResponse> =
 | 
			
		||||
        userService.findAll().map { it.toResponse() }
 | 
			
		||||
 | 
			
		||||
    @GetMapping("/{uuid}")
 | 
			
		||||
    fun findById(@PathVariable uuid: UUID): UserResponse {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
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>  (
 | 
			
		||||
    private val mongoDBClient: MongoDBClient
 | 
			
		||||
@@ -9,23 +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 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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								src/main/kotlin/net/xintanalabs/rssotto/model/App.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/main/kotlin/net/xintanalabs/rssotto/model/App.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
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 name: String,
 | 
			
		||||
    val type: String = "",
 | 
			
		||||
    val source: String,
 | 
			
		||||
    val params: Map<String, String>,
 | 
			
		||||
    val fields: Map<String, String>,
 | 
			
		||||
    val downloadUrl: String,
 | 
			
		||||
    val latestVersion: String? = null,
 | 
			
		||||
    val createdAt: Long = 0,
 | 
			
		||||
    val updatedAt: Long = 0,
 | 
			
		||||
    val lastCheckedAt: Long = 0,
 | 
			
		||||
    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(
 | 
			
		||||
@@ -8,8 +9,3 @@ data class User(
 | 
			
		||||
    val password: String,
 | 
			
		||||
    val role: Role
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
enum class Role {
 | 
			
		||||
    USER, ADMIN
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,40 @@
 | 
			
		||||
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.App
 | 
			
		||||
import org.springframework.data.mongodb.core.query.Criteria
 | 
			
		||||
import org.springframework.stereotype.Repository
 | 
			
		||||
 | 
			
		||||
@Repository
 | 
			
		||||
class AppRepository(
 | 
			
		||||
    mongoDBClient: MongoDBClient
 | 
			
		||||
): MongoDBAbstract<App>(mongoDBClient) {
 | 
			
		||||
    override val collection: String = Constants.COLLECTION_APPS
 | 
			
		||||
    override val entityClass: Class<App> = App::class.java
 | 
			
		||||
 | 
			
		||||
    fun createApp(app: App): App {
 | 
			
		||||
        return createEntity(app)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getAllApps(): List<App> {
 | 
			
		||||
        return getAllEntities()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getAppById(id: String): App? {
 | 
			
		||||
        return getEntityById(id)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun deleteApp(id: String): Long {
 | 
			
		||||
        return deleteEntity(id)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun updateApp(id: String, updateFields: Map<String, Any>): Long {
 | 
			
		||||
        return updateEntity(id, updateFields)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun findAppsByCriteria(criteria: Criteria): List<App> {
 | 
			
		||||
        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,14 +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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,19 +7,21 @@ import net.xintanalabs.rssotto.model.Type
 | 
			
		||||
import org.springframework.stereotype.Repository
 | 
			
		||||
 | 
			
		||||
@Repository
 | 
			
		||||
class TypeRepository(mongoDBClient: MongoDBClient): MongoDBAbstract<Type>(mongoDBClient) {
 | 
			
		||||
class TypeRepository(
 | 
			
		||||
    mongoDBClient: MongoDBClient
 | 
			
		||||
): MongoDBAbstract<Type>(mongoDBClient) {
 | 
			
		||||
    override val collection: String = Constants.COLLECTION_TYPES
 | 
			
		||||
    override val entityClass: Class<Type> = Type::class.java
 | 
			
		||||
 | 
			
		||||
    suspend fun createType(type: Type): Type {
 | 
			
		||||
        return create(type)
 | 
			
		||||
    fun create(type: Type): Type {
 | 
			
		||||
        return createEntity(type)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun getAllTypes(): List<Type> {
 | 
			
		||||
        return getAll()
 | 
			
		||||
    fun getAll(): List<Type> {
 | 
			
		||||
        return getAllEntities()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend 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 ?: ""
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,64 @@
 | 
			
		||||
package net.xintanalabs.rssotto.service
 | 
			
		||||
 | 
			
		||||
import net.xintanalabs.rssotto.model.App
 | 
			
		||||
import net.xintanalabs.rssotto.repository.AppRepository
 | 
			
		||||
import org.springframework.data.mongodb.core.query.Criteria
 | 
			
		||||
import org.springframework.stereotype.Service
 | 
			
		||||
 | 
			
		||||
@Service
 | 
			
		||||
class AppService(
 | 
			
		||||
    private val appRepository: AppRepository
 | 
			
		||||
) {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    fun createApp(app: App): App? {
 | 
			
		||||
        return appRepository.createApp(app)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getAllApps(): List<App> {
 | 
			
		||||
        return appRepository.getAllApps()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getAppById(id: String): App? {
 | 
			
		||||
        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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun updateApp(id: String, updateFields: Map<String, Any>): Long {
 | 
			
		||||
        return appRepository.updateApp(id, updateFields)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun updateAppDetails(id: String, newName: String?): Long {
 | 
			
		||||
        val updateFields = mutableMapOf<String, Any>()
 | 
			
		||||
        newName?.let { updateFields["name"] = it }
 | 
			
		||||
        updateFields["updatedAt"] = System.currentTimeMillis()
 | 
			
		||||
        return appRepository.updateApp(id, updateFields)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun updateLatestVersion(appId: String, latestVersion: String): Long {
 | 
			
		||||
        val updateFields = mapOf(
 | 
			
		||||
            "latestVersion" to latestVersion,
 | 
			
		||||
            "lastCheckedAt" to System.currentTimeMillis()
 | 
			
		||||
        )
 | 
			
		||||
        return appRepository.updateApp(appId, updateFields)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun searchApps(query: String): List<App> {
 | 
			
		||||
        val regex = ".*${Regex.escape(query)}.*"
 | 
			
		||||
        val criteria = Criteria.where("name").regex(regex, "i")
 | 
			
		||||
        return appRepository.findAppsByCriteria(criteria)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
package net.xintanalabs.rssotto.service
 | 
			
		||||
 | 
			
		||||
import net.xintanalabs.rssotto.model.Type
 | 
			
		||||
import net.xintanalabs.rssotto.repository.TypeRepository
 | 
			
		||||
import org.springframework.stereotype.Service
 | 
			
		||||
 | 
			
		||||
@Service
 | 
			
		||||
class TypeService(
 | 
			
		||||
    private val typeRepository: TypeRepository
 | 
			
		||||
) {
 | 
			
		||||
    fun create(type: Type): Type? {
 | 
			
		||||
        return typeRepository.create(type)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun findAll(): List<Type> {
 | 
			
		||||
        return typeRepository.getAll()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun findById(id: String): Type? {
 | 
			
		||||
        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