From c4644d8a0f85e18cc9488c5451835558c5353dff Mon Sep 17 00:00:00 2001 From: Ziedelth Date: Tue, 9 Jan 2024 15:33:55 +0100 Subject: [PATCH] Add DiscordService --- build.gradle.kts | 4 ++ src/main/kotlin/fr/shikkanime/Application.kt | 11 ++- .../admin/AdminConfigController.kt | 5 ++ .../fr/shikkanime/jobs/FetchEpisodesJob.kt | 14 ++-- .../fr/shikkanime/modules/DefaultModule.kt | 3 + .../shikkanime/platforms/AbstractPlatform.kt | 5 +- .../fr/shikkanime/services/ConfigService.kt | 1 + .../fr/shikkanime/services/DiscordService.kt | 68 +++++++++++++++++++ .../fr/shikkanime/services/ImageService.kt | 2 +- .../kotlin/fr/shikkanime/utils/Extensions.kt | 7 ++ .../db/changelog/2024/01/03-changelog.xml | 24 +++++++ .../db/changelog/db.changelog-master.xml | 1 + 12 files changed, 129 insertions(+), 16 deletions(-) create mode 100644 src/main/kotlin/fr/shikkanime/services/DiscordService.kt create mode 100644 src/main/kotlin/fr/shikkanime/utils/Extensions.kt create mode 100644 src/main/resources/db/changelog/2024/01/03-changelog.xml diff --git a/build.gradle.kts b/build.gradle.kts index 766c1116..fb80318e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,5 @@ +import java.net.URI + val ktor_version: String by project val kotlin_version: String by project @@ -19,6 +21,7 @@ application { repositories { mavenCentral() + maven { url = URI("https://jitpack.io") } } dependencies { @@ -58,6 +61,7 @@ dependencies { implementation("com.mortennobel:java-image-scaling:0.8.6") implementation("org.apache.tika:tika-core:3.0.0-BETA") implementation("org.apache.tika:tika-langdetect-optimaize:3.0.0-BETA") + implementation("com.github.discord-jda:JDA:v5.0.0-beta.19") testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version") testImplementation("io.ktor:ktor-client-mock:$ktor_version") testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") diff --git a/src/main/kotlin/fr/shikkanime/Application.kt b/src/main/kotlin/fr/shikkanime/Application.kt index 34c9632d..86ccf6de 100644 --- a/src/main/kotlin/fr/shikkanime/Application.kt +++ b/src/main/kotlin/fr/shikkanime/Application.kt @@ -7,10 +7,7 @@ import fr.shikkanime.jobs.* import fr.shikkanime.plugins.configureHTTP import fr.shikkanime.plugins.configureRouting import fr.shikkanime.plugins.configureSecurity -import fr.shikkanime.services.AnimeService -import fr.shikkanime.services.EpisodeService -import fr.shikkanime.services.ImageService -import fr.shikkanime.services.MemberService +import fr.shikkanime.services.* import fr.shikkanime.utils.* import io.ktor.client.statement.* import io.ktor.http.* @@ -25,14 +22,16 @@ fun main() { logger.info("Starting ShikkAnime...") ImageService.loadCache() + val memberService = Constant.injector.getInstance(MemberService::class.java) val animeService = Constant.injector.getInstance(AnimeService::class.java) val episodeService = Constant.injector.getInstance(EpisodeService::class.java) + val discordService = Constant.injector.getInstance(DiscordService::class.java) + memberService.initDefaultAdminUser() animeService.preIndex() animeService.findAll().forEach { animeService.addImage(it) } episodeService.findAll().forEach { episodeService.addImage(it) } - - Constant.injector.getInstance(MemberService::class.java).initDefaultAdminUser() + discordService.init() // Sync episodes from Jais if (false) { diff --git a/src/main/kotlin/fr/shikkanime/controllers/admin/AdminConfigController.kt b/src/main/kotlin/fr/shikkanime/controllers/admin/AdminConfigController.kt index f8e4b98f..db587fed 100644 --- a/src/main/kotlin/fr/shikkanime/controllers/admin/AdminConfigController.kt +++ b/src/main/kotlin/fr/shikkanime/controllers/admin/AdminConfigController.kt @@ -5,6 +5,7 @@ import fr.shikkanime.converters.AbstractConverter import fr.shikkanime.dtos.ConfigDto import fr.shikkanime.entities.enums.Link import fr.shikkanime.services.ConfigService +import fr.shikkanime.services.DiscordService import fr.shikkanime.utils.routes.AdminSessionAuthenticated import fr.shikkanime.utils.routes.Controller import fr.shikkanime.utils.routes.Path @@ -21,6 +22,9 @@ class AdminConfigController { @Inject private lateinit var configService: ConfigService + @Inject + private lateinit var discordService: DiscordService + @Path @Get @AdminSessionAuthenticated @@ -46,6 +50,7 @@ class AdminConfigController { @AdminSessionAuthenticated private fun postConfig(@PathParam("uuid") uuid: UUID, @BodyParam parameters: Parameters): Response { configService.update(uuid, parameters) + discordService.init() return Response.redirect(Link.CONFIG.href) } } \ No newline at end of file diff --git a/src/main/kotlin/fr/shikkanime/jobs/FetchEpisodesJob.kt b/src/main/kotlin/fr/shikkanime/jobs/FetchEpisodesJob.kt index afe07aec..1c876cdf 100644 --- a/src/main/kotlin/fr/shikkanime/jobs/FetchEpisodesJob.kt +++ b/src/main/kotlin/fr/shikkanime/jobs/FetchEpisodesJob.kt @@ -1,9 +1,13 @@ package fr.shikkanime.jobs +import fr.shikkanime.converters.AbstractConverter +import fr.shikkanime.dtos.EpisodeDto import fr.shikkanime.entities.Episode +import fr.shikkanime.services.DiscordService import fr.shikkanime.services.EpisodeService import fr.shikkanime.utils.Constant import fr.shikkanime.utils.LoggerFactory +import fr.shikkanime.utils.isEqualOrAfter import jakarta.inject.Inject import java.time.ZoneId import java.time.ZonedDateTime @@ -19,6 +23,9 @@ class FetchEpisodesJob : AbstractJob() { @Inject private lateinit var episodeService: EpisodeService + @Inject + private lateinit var discordService: DiscordService + override fun run() { if (isRunning) { logger.warning("Job is already running") @@ -49,14 +56,11 @@ class FetchEpisodesJob : AbstractJob() { } episodes - .filter { - (zonedDateTime.isEqual(it.releaseDateTime) || zonedDateTime.isAfter(it.releaseDateTime)) && !set.contains( - it.hash - ) - } + .filter { (zonedDateTime.isEqualOrAfter(it.releaseDateTime)) && !set.contains(it.hash) } .forEach { val savedEpisode = episodeService.save(it) savedEpisode.hash?.let { hash -> set.add(hash) } + discordService.sendEpisodeRelease(AbstractConverter.convert(savedEpisode, EpisodeDto::class.java)) } isRunning = false diff --git a/src/main/kotlin/fr/shikkanime/modules/DefaultModule.kt b/src/main/kotlin/fr/shikkanime/modules/DefaultModule.kt index 7ee962cd..a97cd951 100644 --- a/src/main/kotlin/fr/shikkanime/modules/DefaultModule.kt +++ b/src/main/kotlin/fr/shikkanime/modules/DefaultModule.kt @@ -5,6 +5,7 @@ import fr.shikkanime.jobs.AbstractJob import fr.shikkanime.platforms.AbstractPlatform import fr.shikkanime.repositories.AbstractRepository import fr.shikkanime.services.AbstractService +import fr.shikkanime.services.DiscordService import fr.shikkanime.utils.Constant import fr.shikkanime.utils.Database import fr.shikkanime.utils.routes.Controller @@ -32,5 +33,7 @@ class DefaultModule : AbstractModule() { Constant.reflections.getTypesAnnotatedWith(Controller::class.java).forEach { bind(it).asEagerSingleton() } + + bind(DiscordService::class.java).asEagerSingleton() } } \ No newline at end of file diff --git a/src/main/kotlin/fr/shikkanime/platforms/AbstractPlatform.kt b/src/main/kotlin/fr/shikkanime/platforms/AbstractPlatform.kt index 090708af..cf13e6ad 100644 --- a/src/main/kotlin/fr/shikkanime/platforms/AbstractPlatform.kt +++ b/src/main/kotlin/fr/shikkanime/platforms/AbstractPlatform.kt @@ -6,6 +6,7 @@ import fr.shikkanime.platforms.configuration.PlatformConfiguration import fr.shikkanime.utils.Constant import fr.shikkanime.utils.LoggerFactory import fr.shikkanime.utils.ObjectParser +import fr.shikkanime.utils.isEqualOrAfter import kotlinx.coroutines.runBlocking import java.io.File import java.lang.reflect.ParameterizedType @@ -62,10 +63,6 @@ abstract class AbstractPlatform, K : Any, V> { file.writeText(ObjectParser.toJson(configuration)) } - private fun ZonedDateTime.isEqualOrAfter(other: ZonedDateTime): Boolean { - return this.isEqual(other) || this.isAfter(other) - } - private fun getConfigurationFile(): File { val folder = File(Constant.dataFolder, "config") if (!folder.exists()) folder.mkdirs() diff --git a/src/main/kotlin/fr/shikkanime/services/ConfigService.kt b/src/main/kotlin/fr/shikkanime/services/ConfigService.kt index 361d8025..9a617138 100644 --- a/src/main/kotlin/fr/shikkanime/services/ConfigService.kt +++ b/src/main/kotlin/fr/shikkanime/services/ConfigService.kt @@ -16,6 +16,7 @@ class ConfigService : AbstractService() { fun findByName(name: String) = configRepository.findByName(name) + fun getValueAsString(name: String) = findByName(name)?.propertyValue fun getValueAsInt(name: String) = findByName(name)?.propertyValue?.toIntOrNull() fun update(uuid: UUID, parameters: Parameters): Config? { diff --git a/src/main/kotlin/fr/shikkanime/services/DiscordService.kt b/src/main/kotlin/fr/shikkanime/services/DiscordService.kt new file mode 100644 index 00000000..75a4817b --- /dev/null +++ b/src/main/kotlin/fr/shikkanime/services/DiscordService.kt @@ -0,0 +1,68 @@ +package fr.shikkanime.services + +import com.google.inject.Inject +import fr.shikkanime.dtos.EpisodeDto +import fr.shikkanime.utils.LoggerFactory +import fr.shikkanime.utils.StringUtils +import net.dv8tion.jda.api.EmbedBuilder +import net.dv8tion.jda.api.JDA +import net.dv8tion.jda.api.JDABuilder +import net.dv8tion.jda.api.entities.Activity +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel +import java.net.URI +import java.time.ZonedDateTime +import java.util.logging.Level +import javax.imageio.ImageIO + +class DiscordService { + @Inject + private lateinit var configService: ConfigService + + private val logger = LoggerFactory.getLogger(DiscordService::class.java) + private var isInitialized = false + private var jda: JDA? = null + + fun init() { + if (isInitialized) return + + try { + val builder = JDABuilder.createDefault(configService.getValueAsString("discord_token")) + builder.setActivity(Activity.playing("https://www.shikkanime.fr/")) + jda = builder.build() + jda?.awaitReady() + isInitialized = true + } catch (e: Exception) { + logger.log(Level.SEVERE, "Error while initializing DiscordService", e) + } + } + + private fun getTextChannels(): MutableList? = + jda?.getTextChannelsByName("bot\uD83E\uDD16", true) + + fun sendEpisodeRelease(episodeDto: EpisodeDto) { + init() + if (!isInitialized) return + if (episodeDto.image.isBlank()) return + + try { + val embedMessage = EmbedBuilder() + val image = ImageIO.read(URI(episodeDto.image).toURL()) + embedMessage.setColor(ImageService.getDominantColor(image)) + embedMessage.setAuthor( + episodeDto.platform.platformName, + episodeDto.platform.url, + "https://www.shikkanime.fr/admin/assets/img/platforms/${episodeDto.platform.image}" + ) + embedMessage.setTitle(episodeDto.anime.shortName, episodeDto.url) + embedMessage.setDescription("**${episodeDto.title ?: "Untitled"}**\n${StringUtils.toEpisodeString(episodeDto)}") + embedMessage.setImage(episodeDto.image) + embedMessage.setFooter("Shikkanime", "https://www.shikkanime.fr/admin/assets/favicons/favicon-64x64.png") + embedMessage.setTimestamp(ZonedDateTime.parse(episodeDto.releaseDateTime).toInstant()) + val embed = embedMessage.build() + + getTextChannels()?.forEach { it.sendMessageEmbeds(embed).queue() } + } catch (e: Exception) { + logger.log(Level.SEVERE, "Error while sending message to Discord", e) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/fr/shikkanime/services/ImageService.kt b/src/main/kotlin/fr/shikkanime/services/ImageService.kt index 6962aec9..fc9d3ed8 100644 --- a/src/main/kotlin/fr/shikkanime/services/ImageService.kt +++ b/src/main/kotlin/fr/shikkanime/services/ImageService.kt @@ -179,7 +179,7 @@ object ImageService { operator fun get(uuid: UUID): Image? = cache.find { it.uuid == uuid.toString() } - private fun getDominantColor(image: BufferedImage): Color { + fun getDominantColor(image: BufferedImage): Color { val pixels = IntArray(image.width * image.height).apply { image.getRGB( 0, diff --git a/src/main/kotlin/fr/shikkanime/utils/Extensions.kt b/src/main/kotlin/fr/shikkanime/utils/Extensions.kt new file mode 100644 index 00000000..77404b31 --- /dev/null +++ b/src/main/kotlin/fr/shikkanime/utils/Extensions.kt @@ -0,0 +1,7 @@ +package fr.shikkanime.utils + +import java.time.ZonedDateTime + +fun ZonedDateTime.isEqualOrAfter(other: ZonedDateTime): Boolean { + return this.isEqual(other) || this.isAfter(other) +} \ No newline at end of file diff --git a/src/main/resources/db/changelog/2024/01/03-changelog.xml b/src/main/resources/db/changelog/2024/01/03-changelog.xml new file mode 100644 index 00000000..c59b1f38 --- /dev/null +++ b/src/main/resources/db/changelog/2024/01/03-changelog.xml @@ -0,0 +1,24 @@ + + + + + + + + SELECT COUNT(*) + FROM config + WHERE property_key = 'discord_token' + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/db/changelog/db.changelog-master.xml b/src/main/resources/db/changelog/db.changelog-master.xml index 605157c8..e057d266 100644 --- a/src/main/resources/db/changelog/db.changelog-master.xml +++ b/src/main/resources/db/changelog/db.changelog-master.xml @@ -9,4 +9,5 @@ + \ No newline at end of file