diff --git a/build.gradle.kts b/build.gradle.kts index f1864c4..cda7cbc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,6 +23,8 @@ dependencies { implementation(libs.kx.ser) implementation(libs.twitch4j) implementation(libs.events4j) + implementation(libs.kx.coroutines) + implementation(libs.kmongo) // Logging dependencies implementation(libs.groovy) diff --git a/detekt.yml b/detekt.yml index a0a0f5f..3a3fc2b 100644 --- a/detekt.yml +++ b/detekt.yml @@ -560,7 +560,7 @@ style: active: true maxJumpCount: 3 MagicNumber: - active: true + active: false excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] ignoreNumbers: [ '-1', '0', '1', '2' ] ignoreHashCodeFunction: true @@ -625,7 +625,7 @@ style: TrailingWhitespace: active: true UnderscoresInNumericLiterals: - active: true + active: false acceptableDecimalLength: 5 UnnecessaryAbstractClass: active: true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 79f1c0d..9515f66 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,9 @@ kordex-gradle = "1.7.0" kordex = "2.3.1-SNAPSHOT" twitch4j = "1.24.0" events4j = "0.12.2" +kx-coroutines = "1.9.0" +kmongo = "5.1.0" + [libraries] @@ -25,6 +28,10 @@ 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-reactor", version.ref = "events4j" } +kmongo = { module="org.litote.kmongo:kmongo-coroutine-serialization", version.ref = "kmongo" } +kx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kx-coroutines" } + + [plugins] diff --git a/src/main/kotlin/dev/jansel/aglaea/App.kt b/src/main/kotlin/dev/jansel/aglaea/App.kt index 708345c..fdd1337 100644 --- a/src/main/kotlin/dev/jansel/aglaea/App.kt +++ b/src/main/kotlin/dev/jansel/aglaea/App.kt @@ -4,7 +4,7 @@ package dev.jansel.aglaea import com.github.twitch4j.TwitchClient -import dev.jansel.aglaea.extensions.TestExtension +import dev.jansel.aglaea.extensions.ReplayExtension import dev.jansel.aglaea.utils.twitch import dev.kord.common.entity.Snowflake import dev.kordex.core.ExtensibleBot @@ -25,7 +25,7 @@ private val TOKEN = env("TOKEN") // Get the bot' token from the env vars or a suspend fun main() { val bot = ExtensibleBot(TOKEN) { extensions { - add(::TestExtension) + add(::ReplayExtension) } twitch(true) if (devMode) { diff --git a/src/main/kotlin/dev/jansel/aglaea/database/Database.kt b/src/main/kotlin/dev/jansel/aglaea/database/Database.kt new file mode 100644 index 0000000..8edd959 --- /dev/null +++ b/src/main/kotlin/dev/jansel/aglaea/database/Database.kt @@ -0,0 +1,24 @@ +package dev.jansel.aglaea.database + +import com.mongodb.ConnectionString +import com.mongodb.MongoClientSettings +import dev.jansel.aglaea.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("Aglaea") + + suspend fun migrate() { + Migrator.migrate() + } +} diff --git a/src/main/kotlin/dev/jansel/aglaea/database/Migrator.kt b/src/main/kotlin/dev/jansel/aglaea/database/Migrator.kt new file mode 100644 index 0000000..5b9fe7e --- /dev/null +++ b/src/main/kotlin/dev/jansel/aglaea/database/Migrator.kt @@ -0,0 +1,71 @@ +package dev.jansel.aglaea.database + +import com.github.philippheuer.events4j.reactor.ReactorEventHandler +import com.github.twitch4j.TwitchClientBuilder +import dev.jansel.aglaea.database.collections.MetaCollection +import dev.jansel.aglaea.database.entities.MetaData +import dev.jansel.aglaea.database.migrations.v1 +import dev.jansel.aglaea.twitchClient +import dev.jansel.aglaea.utils.twitchcid +import dev.jansel.aglaea.utils.twitchcs +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" } + logger.info { "Initializing Twitch client just in case" } + twitchClient = TwitchClientBuilder.builder() + .withEnableHelix(true) + .withDefaultEventHandler(ReactorEventHandler::class.java) + .withClientId(twitchcid) + .withClientSecret(twitchcs) + .build() + + 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/aglaea/database/collections/MetaCollection.kt b/src/main/kotlin/dev/jansel/aglaea/database/collections/MetaCollection.kt new file mode 100644 index 0000000..a7429ad --- /dev/null +++ b/src/main/kotlin/dev/jansel/aglaea/database/collections/MetaCollection.kt @@ -0,0 +1,26 @@ +package dev.jansel.aglaea.database.collections + +import dev.jansel.aglaea.database.Database +import dev.jansel.aglaea.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/aglaea/database/collections/ReplayCollection.kt b/src/main/kotlin/dev/jansel/aglaea/database/collections/ReplayCollection.kt new file mode 100644 index 0000000..7d3c7af --- /dev/null +++ b/src/main/kotlin/dev/jansel/aglaea/database/collections/ReplayCollection.kt @@ -0,0 +1,30 @@ +package dev.jansel.aglaea.database.collections + +import dev.jansel.aglaea.database.Database +import dev.jansel.aglaea.database.entities.ReplayData +import dev.kordex.core.koin.KordExKoinComponent +import org.koin.core.component.inject +import org.litote.kmongo.eq +import org.litote.kmongo.exists + +class ReplayCollection : KordExKoinComponent { + private val db by inject() + + @PublishedApi + internal val collection = db.mongo.getCollection() + suspend fun getAll(): List = + collection.find().toList() + suspend fun get(id: String): ReplayData? = + collection.findOne(ReplayData::id eq id) + suspend fun set(replay: ReplayData) = + collection.insertOne(replay) + suspend fun update(replay: ReplayData) = + collection.findOneAndReplace( + ReplayData::id eq replay.id, + replay + ) + suspend fun delete(id: String) = + collection.deleteOne(ReplayData::id eq id) + suspend fun deleteAll() = + collection.deleteMany(ReplayData::id exists true) +} diff --git a/src/main/kotlin/dev/jansel/aglaea/database/entities/MetaData.kt b/src/main/kotlin/dev/jansel/aglaea/database/entities/MetaData.kt new file mode 100644 index 0000000..1b4b4cf --- /dev/null +++ b/src/main/kotlin/dev/jansel/aglaea/database/entities/MetaData.kt @@ -0,0 +1,9 @@ +package dev.jansel.aglaea.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/aglaea/database/entities/ReplayData.kt b/src/main/kotlin/dev/jansel/aglaea/database/entities/ReplayData.kt new file mode 100644 index 0000000..751d6fb --- /dev/null +++ b/src/main/kotlin/dev/jansel/aglaea/database/entities/ReplayData.kt @@ -0,0 +1,27 @@ +package dev.jansel.aglaea.database.entities + +import kotlinx.serialization.Serializable + +@Serializable +data class ReplayData( + val id: String, + val replayFile: ByteArray, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ReplayData + + if (id != other.id) return false + if (!replayFile.contentEquals(other.replayFile)) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + replayFile.contentHashCode() + return result + } +} diff --git a/src/main/kotlin/dev/jansel/aglaea/database/migrations/v1.kt b/src/main/kotlin/dev/jansel/aglaea/database/migrations/v1.kt new file mode 100644 index 0000000..3938741 --- /dev/null +++ b/src/main/kotlin/dev/jansel/aglaea/database/migrations/v1.kt @@ -0,0 +1,7 @@ +package dev.jansel.aglaea.database.migrations + +import org.litote.kmongo.coroutine.CoroutineDatabase + +suspend fun v1(db: CoroutineDatabase) { + db.createCollection("metaData") +} diff --git a/src/main/kotlin/dev/jansel/aglaea/extensions/EventHooks.kt b/src/main/kotlin/dev/jansel/aglaea/extensions/EventHooks.kt index 454b0f4..5957cbb 100644 --- a/src/main/kotlin/dev/jansel/aglaea/extensions/EventHooks.kt +++ b/src/main/kotlin/dev/jansel/aglaea/extensions/EventHooks.kt @@ -1,10 +1,6 @@ package dev.jansel.aglaea.extensions -import com.github.philippheuer.credentialmanager.domain.OAuth2Credential import dev.jansel.aglaea.logger -import dev.jansel.aglaea.twitchClient -import dev.jansel.aglaea.utils.twitchcid -import dev.jansel.aglaea.utils.twitchcs import dev.kord.core.event.gateway.ReadyEvent import dev.kordex.core.extensions.Extension import dev.kordex.core.extensions.event @@ -18,7 +14,6 @@ class EventHooks : Extension() { // This is where you can add any code that should run when the bot is ready // For example, you can initialize any services or start any background tasks logger.info { "Bot is ready!" } - twitchClient!!.pubSub.listenForChannelPointsRedemptionEvents(OAuth2Credential(twitchcid, twitchcs), "120275141") } } } diff --git a/src/main/kotlin/dev/jansel/aglaea/extensions/ReplayExtension.kt b/src/main/kotlin/dev/jansel/aglaea/extensions/ReplayExtension.kt new file mode 100644 index 0000000..29a9a02 --- /dev/null +++ b/src/main/kotlin/dev/jansel/aglaea/extensions/ReplayExtension.kt @@ -0,0 +1,49 @@ +package dev.jansel.aglaea.extensions + +import dev.jansel.aglaea.database.collections.ReplayCollection +import dev.jansel.aglaea.database.entities.ReplayData +import dev.jansel.aglaea.i18n.Translations +import dev.kord.common.entity.Snowflake +import dev.kordex.core.checks.inChannel +import dev.kordex.core.commands.Arguments +import dev.kordex.core.commands.converters.impl.attachment +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.ephemeralSlashCommand +import dev.kordex.core.utils.download +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +class ReplayExtension : Extension() { + override val name = "test" + + @OptIn(ExperimentalUnsignedTypes::class, ExperimentalUuidApi::class) + override suspend fun setup() { + ephemeralSlashCommand(::ReplayArgs) { + check { + inChannel(Snowflake(1130954956892029060)) + } + action { + val file = arguments.file + if (file.filename.endsWith(".osr")) { + val id = Uuid.random().toString() + + ReplayCollection().set(ReplayData(id, file.download())) + respond { + content = "Your Auth Code is: $id" + } + } else { + respond { + content = "" + } + } + } + } + } + + inner class ReplayArgs : Arguments() { + val file by attachment { + name = Translations.Arguments.File.name + description = Translations.Arguments.File.description + } + } +} diff --git a/src/main/kotlin/dev/jansel/aglaea/extensions/TestExtension.kt b/src/main/kotlin/dev/jansel/aglaea/extensions/TestExtension.kt deleted file mode 100644 index 6de0749..0000000 --- a/src/main/kotlin/dev/jansel/aglaea/extensions/TestExtension.kt +++ /dev/null @@ -1,97 +0,0 @@ -package dev.jansel.aglaea.extensions - -import dev.jansel.aglaea.TEST_SERVER_ID -import dev.jansel.aglaea.i18n.Translations -import dev.kordex.core.commands.Arguments -import dev.kordex.core.commands.converters.impl.coalescingDefaultingString -import dev.kordex.core.commands.converters.impl.defaultingString -import dev.kordex.core.commands.converters.impl.user -import dev.kordex.core.components.components -import dev.kordex.core.components.publicButton -import dev.kordex.core.extensions.Extension -import dev.kordex.core.extensions.publicSlashCommand -import dev.kordex.core.i18n.withContext - -class TestExtension : Extension() { - override val name = "test" - - override suspend fun setup() { - publicSlashCommand(::SlapSlashArgs) { - name = Translations.Commands.Slap.name - description = Translations.Commands.Slap.description - - guild(TEST_SERVER_ID) // Otherwise it will take up to an hour to update - - action { - // Don't slap ourselves on request, slap the requester! - val realTarget = if (arguments.target.id == event.kord.selfId) { - member - } else { - arguments.target - } - - respond { - content = Translations.Commands.Slap.response - .withContext(this@action) - .translateNamed( - "target" to realTarget?.mention, - "weapon" to arguments.weapon - ) - } - } - } - - publicSlashCommand { - name = Translations.Commands.Button.name - description = Translations.Commands.Button.description - - action { - respond { - components { - publicButton { - label = Translations.Components.Button.label - .withLocale(this@action.getLocale()) - - action { - respond { - content = Translations.Components.Button.response - .withLocale(getLocale()) - .translate() - } - } - } - } - } - } - } - } - - inner class SlapArgs : Arguments() { - val target by user { - name = Translations.Arguments.Target.name - description = Translations.Arguments.Target.description - } - - val weapon by coalescingDefaultingString { - name = Translations.Arguments.Weapon.name - - defaultValue = "🐟" - description = Translations.Arguments.Weapon.description - } - } - - inner class SlapSlashArgs : Arguments() { - val target by user { - name = Translations.Arguments.Target.name - description = Translations.Arguments.Target.description - } - - // Slash commands don't support coalescing strings - val weapon by defaultingString { - name = Translations.Arguments.Weapon.name - - defaultValue = "🐟" - description = Translations.Arguments.Weapon.description - } - } -} diff --git a/src/main/kotlin/dev/jansel/aglaea/utils/Twitch.kt b/src/main/kotlin/dev/jansel/aglaea/utils/Twitch.kt index 92e1c63..021cd91 100644 --- a/src/main/kotlin/dev/jansel/aglaea/utils/Twitch.kt +++ b/src/main/kotlin/dev/jansel/aglaea/utils/Twitch.kt @@ -3,7 +3,6 @@ package dev.jansel.aglaea.utils import com.github.philippheuer.credentialmanager.domain.OAuth2Credential import com.github.twitch4j.TwitchClientBuilder import com.github.twitch4j.chat.events.channel.ChannelMessageEvent -import com.github.twitch4j.graphql.internal.type.CommunityPointsCustomRewardRedemptionStatus import dev.jansel.aglaea.logger import dev.jansel.aglaea.twitchClient import dev.kordex.core.koin.KordExKoinComponent @@ -24,12 +23,6 @@ class Twitch : KordExKoinComponent { twitchClient!!.eventManager.onEvent(ChannelMessageEvent::class.java) { event -> if (event.customRewardId.isPresent && event.customRewardId.get() == "38157e62-de35-4a21-8200-447b55d7577e") { logger.info { "Channel points redeemed: ${event.customRewardId.get()}" } - twitchClient!!.graphQL.updateRedemptionStatus( - OAuth2Credential("twitch", mainTwitchToken), - "120275141", - event.eventId, - CommunityPointsCustomRewardRedemptionStatus.FULFILLED - ).execute() } } } diff --git a/src/main/kotlin/dev/jansel/aglaea/utils/_Utils.kt b/src/main/kotlin/dev/jansel/aglaea/utils/_Utils.kt index d8a067a..3f364bb 100644 --- a/src/main/kotlin/dev/jansel/aglaea/utils/_Utils.kt +++ b/src/main/kotlin/dev/jansel/aglaea/utils/_Utils.kt @@ -9,9 +9,8 @@ import org.koin.dsl.bind val twitchcid = env("TWITCH_CLIENT_ID") val twitchcs = env("TWITCH_CLIENT_SECRET") val twitchToken = env("TWITCH_OAUTH_TOKEN") +val mongoUri = env("MONGO_URI") -// This is the token for the main account, because of ChannelPoint stuff -val mainTwitchToken = env("TWITCH_OAUTH_TOKEN_MAIN") suspend inline fun ExtensibleBotBuilder.twitch(active: Boolean) { hooks { beforeKoinSetup { diff --git a/src/main/resources/translations/aglaea/strings.properties b/src/main/resources/translations/aglaea/strings.properties index bdc9bc1..9ca975f 100644 --- a/src/main/resources/translations/aglaea/strings.properties +++ b/src/main/resources/translations/aglaea/strings.properties @@ -8,8 +8,5 @@ commands.button.description=A simple example command, which sends a button. components.button.label=Button! components.button.response=You pushed the button! -arguments.target.name=target -arguments.target.description=User you want to slap - -arguments.weapon.name=weapon -arguments.weapon.description=What you want to slap with +arguments.file.name=file +arguments.file.description=The file to upload