diff --git a/build.gradle.kts b/build.gradle.kts index fdf0e57..4746407 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation(libs.kx.ser) implementation(libs.kx.coroutines) implementation(libs.twitch4j) + implementation(libs.kmongo) // Logging dependencies implementation(libs.groovy) @@ -32,6 +33,8 @@ dependencies { kordEx { kordExVersion = "2.3.1-SNAPSHOT" + jvmTarget = 21 + bot { // See https://docs.kordex.dev/data-collection.html dataCollection(DataCollection.Standard) @@ -79,3 +82,8 @@ docker { ) } } + +tasks.wrapper { + gradleVersion = "8.10.2" + distributionType = Wrapper.DistributionType.BIN +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5c5c7ab..77e9e2f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,7 @@ logging = "7.0.0" twitch4j = "1.22.0" events4j = "0.12.2" kx-coroutines = "1.9.0" +kmongo = "4.9.0" [libraries] groovy = { module = "org.codehaus.groovy:groovy", version.ref = "groovy" } @@ -22,3 +23,4 @@ logback-groovy = { module = "io.github.virtualdogbert:logback-groovy-config", ve logging = { module = "io.github.oshai:kotlin-logging", version.ref = "logging" } twitch4j = { module = "com.github.twitch4j:twitch4j", version.ref = "twitch4j" } events4j = { module = "com.github.philippheuer.events4j:events4j-handler-simple", version.ref = "events4j" } +kmongo = { module="org.litote.kmongo:kmongo-coroutine-serialization", version.ref = "kmongo"} diff --git a/src/main/kotlin/dev/jansel/feixiao/App.kt b/src/main/kotlin/dev/jansel/feixiao/App.kt index c581946..27f8b05 100644 --- a/src/main/kotlin/dev/jansel/feixiao/App.kt +++ b/src/main/kotlin/dev/jansel/feixiao/App.kt @@ -3,20 +3,48 @@ */ package dev.jansel.feixiao +import com.github.twitch4j.TwitchClientBuilder +import com.github.twitch4j.events.ChannelChangeTitleEvent +import com.github.twitch4j.events.ChannelGoLiveEvent +import com.github.twitch4j.events.ChannelGoOfflineEvent import dev.jansel.feixiao.extensions.EventHooks import dev.jansel.feixiao.extensions.MessageEvents import dev.jansel.feixiao.utils.* -import dev.kordex.core.DATA_COLLECTION +import dev.kord.core.behavior.getChannelOf +import dev.kord.core.entity.channel.GuildMessageChannel import dev.kordex.core.ExtensibleBot import dev.kordex.data.api.DataCollection +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking suspend fun main() { val bot = ExtensibleBot(token) { + database(true) dataCollectionMode = DataCollection.None extensions { add(::MessageEvents) add(::EventHooks) } } + val twitchClient = TwitchClientBuilder.builder() + .withEnableHelix(true) + .withClientId(twitchcid) + .withClientSecret(twitchcs) + .build() + + twitchClient.clientHelper.enableStreamEventListener("janselosu") + + twitchClient.eventManager.onEvent(ChannelGoLiveEvent::class.java) { + println("${it.channel.name} went live!") + runBlocking { + launch { + val twitchpingschannel = + bot.kordRef.getGuildOrNull(tserverid)?.getChannelOf(tchannelid) + twitchpingschannel?.createMessage("<@&1130981452130037800> ${it.channel.name} is now live at https://twitch.tv/${it.channel.name} streaming ${it.stream.gameName}: ${it.stream.title}") + bot.kordRef.editPresence { streaming(it.stream.title, "https://twitch.tv/${it.channel.name}") } + } + } + } + bot.start() } diff --git a/src/main/kotlin/dev/jansel/feixiao/database/Database.kt b/src/main/kotlin/dev/jansel/feixiao/database/Database.kt new file mode 100644 index 0000000..d733a4f --- /dev/null +++ b/src/main/kotlin/dev/jansel/feixiao/database/Database.kt @@ -0,0 +1,24 @@ +package dev.jansel.feixiao.database + +import com.mongodb.ConnectionString +import com.mongodb.MongoClientSettings +import dev.jansel.feixiao.utils.mongoUri +import org.bson.UuidRepresentation +import org.litote.kmongo.coroutine.coroutine +import org.litote.kmongo.reactivestreams.KMongo + +class Database { + private val settings = MongoClientSettings + .builder() + .uuidRepresentation(UuidRepresentation.STANDARD) + .applyConnectionString(ConnectionString(mongoUri)) + .build() + + private val client = KMongo.createClient(settings).coroutine + + val mongo get() = client.getDatabase("Feixiao") + + suspend fun migrate() { + Migrator.migrate() + } +} diff --git a/src/main/kotlin/dev/jansel/feixiao/database/Migrator.kt b/src/main/kotlin/dev/jansel/feixiao/database/Migrator.kt new file mode 100644 index 0000000..c93d585 --- /dev/null +++ b/src/main/kotlin/dev/jansel/feixiao/database/Migrator.kt @@ -0,0 +1,59 @@ +package dev.jansel.feixiao.database + +import dev.jansel.feixiao.database.collections.MetaCollection +import dev.jansel.feixiao.database.entities.MetaData +import dev.jansel.feixiao.database.migrations.v1 +import dev.kordex.core.koin.KordExKoinComponent +import io.github.oshai.kotlinlogging.KotlinLogging +import org.koin.core.component.inject + +object Migrator : KordExKoinComponent { + private val logger = KotlinLogging.logger("Migrator Logger") + + private val db: Database by inject() + private val mainMetaCollection: MetaCollection by inject() + + suspend fun migrate() { + logger.info { "Starting main database migration" } + + var meta = mainMetaCollection.get() + + if (meta == null) { + meta = MetaData(0) + + mainMetaCollection.set(meta) + } + + var currentVersion = meta.version + + logger.info { "Current main database version: v$currentVersion" } + + while (true) { + val nextVersion = currentVersion + 1 + + @Suppress("TooGenericExceptionCaught", "UseIfInsteadOfWhen") + try { + when (nextVersion) { + 1 -> ::v1 + else -> break + } (db.mongo) + + logger.info { "Migrated database to version $nextVersion." } + } catch (t: Throwable) { + logger.error(t) { "Failed to migrate database to version $nextVersion." } + + throw t + } + + currentVersion = nextVersion + } + + if (currentVersion != meta.version) { + meta = meta.copy(version = currentVersion) + + mainMetaCollection.update(meta) + + logger.info { "Finished main database migrations." } + } + } +} diff --git a/src/main/kotlin/dev/jansel/feixiao/database/collections/MetaCollection.kt b/src/main/kotlin/dev/jansel/feixiao/database/collections/MetaCollection.kt new file mode 100644 index 0000000..ac35802 --- /dev/null +++ b/src/main/kotlin/dev/jansel/feixiao/database/collections/MetaCollection.kt @@ -0,0 +1,26 @@ +package dev.jansel.feixiao.database.collections + +import dev.jansel.feixiao.database.Database +import dev.jansel.feixiao.database.entities.MetaData +import dev.kordex.core.koin.KordExKoinComponent +import org.koin.core.component.inject +import org.litote.kmongo.eq + +class MetaCollection : KordExKoinComponent { + private val db: Database by inject() + + @PublishedApi + internal val collection = db.mongo.getCollection() + + suspend fun get(): MetaData? = + collection.findOne() + + suspend fun set(meta: MetaData) = + collection.insertOne(meta) + + suspend fun update(meta: MetaData) = + collection.findOneAndReplace( + MetaData::id eq "meta", + meta + ) +} diff --git a/src/main/kotlin/dev/jansel/feixiao/database/entities/MetaData.kt b/src/main/kotlin/dev/jansel/feixiao/database/entities/MetaData.kt new file mode 100644 index 0000000..07cda60 --- /dev/null +++ b/src/main/kotlin/dev/jansel/feixiao/database/entities/MetaData.kt @@ -0,0 +1,9 @@ +package dev.jansel.feixiao.database.entities + +import kotlinx.serialization.Serializable + +@Serializable +data class MetaData( + val version: Int, + val id: String = "meta" +) diff --git a/src/main/kotlin/dev/jansel/feixiao/database/migrations/v1.kt b/src/main/kotlin/dev/jansel/feixiao/database/migrations/v1.kt new file mode 100644 index 0000000..d09b7ba --- /dev/null +++ b/src/main/kotlin/dev/jansel/feixiao/database/migrations/v1.kt @@ -0,0 +1,7 @@ +package dev.jansel.feixiao.database.migrations + +import org.litote.kmongo.coroutine.CoroutineDatabase + +suspend fun v1(db: CoroutineDatabase) { + db.createCollection("meta") +} diff --git a/src/main/kotlin/dev/jansel/feixiao/extensions/EventHooks.kt b/src/main/kotlin/dev/jansel/feixiao/extensions/EventHooks.kt index 09a65a9..342e28e 100644 --- a/src/main/kotlin/dev/jansel/feixiao/extensions/EventHooks.kt +++ b/src/main/kotlin/dev/jansel/feixiao/extensions/EventHooks.kt @@ -1,20 +1,8 @@ package dev.jansel.feixiao.extensions -import com.github.twitch4j.TwitchClientBuilder -import com.github.twitch4j.events.ChannelChangeTitleEvent -import com.github.twitch4j.events.ChannelGoLiveEvent -import com.github.twitch4j.events.ChannelGoOfflineEvent -import dev.jansel.feixiao.utils.tchannelid -import dev.jansel.feixiao.utils.tserverid -import dev.jansel.feixiao.utils.twitchcid -import dev.jansel.feixiao.utils.twitchcs -import dev.kord.core.behavior.getChannelOf -import dev.kord.core.entity.channel.GuildMessageChannel import dev.kord.core.event.gateway.ReadyEvent import dev.kordex.core.extensions.Extension import dev.kordex.core.extensions.event -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking class EventHooks : Extension() { override val name = "eventhooks" @@ -23,40 +11,7 @@ class EventHooks : Extension() { event { action { println("Bot is ready!") - kord.editPresence { playing("osu!") } - val twitchClient = TwitchClientBuilder.builder() - .withEnableHelix(true) - .withClientId(twitchcid) - .withClientSecret(twitchcs) - .build() - twitchClient.clientHelper.enableStreamEventListener("janselosu") - twitchClient.eventManager.onEvent(ChannelGoLiveEvent::class.java) { - println("${it.channel.name} went live!") - runBlocking { - launch { - val twitchpingschannel = - kord.getGuildOrNull(tserverid)?.getChannelOf(tchannelid) - twitchpingschannel?.createMessage("<@&1130981452130037800> ${it.channel.name} is now live at https://twitch.tv/${it.channel.name} streaming ${it.stream.gameName}: ${it.stream.title}") - kord.editPresence { streaming(it.stream.title, "https://twitch.tv/${it.channel.name}") } - } - } - } - twitchClient.eventManager.onEvent(ChannelGoOfflineEvent::class.java) { - println("${it.channel.name} went offline.") - runBlocking { - launch { - kord.editPresence { playing("osu!") } - } - } - } - twitchClient.eventManager.onEvent(ChannelChangeTitleEvent::class.java) { - println("Title changed to ${it.title}") - runBlocking { - launch { - kord.editPresence { streaming(it.title, "https://twitch.tv/${it.channel.name}") } - } - } - } + kord.editPresence { "electing a president..." } } } } diff --git a/src/main/kotlin/dev/jansel/feixiao/utils/_Utils.kt b/src/main/kotlin/dev/jansel/feixiao/utils/_Utils.kt index 3b6413b..571e0f8 100644 --- a/src/main/kotlin/dev/jansel/feixiao/utils/_Utils.kt +++ b/src/main/kotlin/dev/jansel/feixiao/utils/_Utils.kt @@ -1,10 +1,37 @@ package dev.jansel.feixiao.utils +import dev.jansel.feixiao.database.Database +import dev.jansel.feixiao.database.collections.MetaCollection import dev.kord.common.entity.Snowflake +import dev.kordex.core.builders.ExtensibleBotBuilder import dev.kordex.core.utils.env +import dev.kordex.core.utils.loadModule +import kotlinx.coroutines.runBlocking +import org.koin.dsl.bind val twitchcid = env("TWITCH_CLIENT_ID") val twitchcs = env("TWITCH_CLIENT_SECRET") val token = env("TOKEN") val tserverid = Snowflake(env("TEST_SERVER").toLong()) val tchannelid = Snowflake(env("TEST_CHANNEL").toLong()) +val mongoUri = env("MONGO_URI") + +suspend inline fun ExtensibleBotBuilder.database(migrate: Boolean) { + val db = Database() + + hooks { + beforeKoinSetup { + loadModule { + single { db } bind Database::class + } + + loadModule { + single { MetaCollection() } bind MetaCollection::class + } + + if (migrate) { + runBlocking { db.migrate() } + } + } + } +}