1
0

banger
All checks were successful
Build & Publish / build (push) Successful in 3m45s

This commit is contained in:
Jannik Reimers 2025-04-03 22:44:28 +02:00
parent 5ad150e53d
commit 61c9fa6731
Signed by: jansel
GPG Key ID: 39C62D7D5233CFD0
17 changed files with 259 additions and 120 deletions

View File

@ -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)

View File

@ -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

View File

@ -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]

View File

@ -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) {

View File

@ -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()
}
}

View File

@ -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." }
}
}
}

View File

@ -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<MetaData>()
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
)
}

View File

@ -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<Database>()
@PublishedApi
internal val collection = db.mongo.getCollection<ReplayData>()
suspend fun getAll(): List<ReplayData> =
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)
}

View File

@ -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"
)

View File

@ -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
}
}

View File

@ -0,0 +1,7 @@
package dev.jansel.aglaea.database.migrations
import org.litote.kmongo.coroutine.CoroutineDatabase
suspend fun v1(db: CoroutineDatabase) {
db.createCollection("metaData")
}

View File

@ -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")
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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()
}
}
}

View File

@ -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 {

View File

@ -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