diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
index e122a944c..fbadf672c 100644
--- a/.github/workflows/gradle.yml
+++ b/.github/workflows/gradle.yml
@@ -14,7 +14,7 @@ jobs:
if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name != 'DreamExposure/DisCal-Discord-Bot' }}
strategy:
matrix:
- java: [ 16, 17 ]
+ java: [ 17 ]
steps:
- uses: actions/checkout@v2
@@ -93,8 +93,8 @@ jobs:
# Have K8S pull latest images for dev pods
- name: Trigger dev deploy
- uses: Consensys/kubernetes-action@master
+ uses: davi020/kubernetes-action@master
env:
KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_DATA }}
with:
- args: delete -n discal pods -l profile=dev,app=discal
+ args: delete -n discal pods -l env=dev,app=discal
diff --git a/README.md b/README.md
index 0f8cabde6..fe41f9dcd 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# DisCal
[![Discord](https://img.shields.io/discord/375357265198317579?label=DreamExposure&style=flat-square)](https://discord.gg/2TFqyuy)
-![GitHub Workflow Status](https://img.shields.io/github/workflow/status/DreamExposure/DisCal-Discord-Bot/Java%20CI?label=Build&style=flat-square)
+![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/DreamExposure/DisCal-Discord-Bot/gradle.yml?branch=develop&label=Build&style=flat-square)
[![Website](https://img.shields.io/website?down_color=red&down_message=offline&label=Status&style=flat-square&up_message=online&url=https%3A%2F%2Fwww.discalbot.com)](https://discalbot.com)
DisCal is a discord bot that connects Discord and Google Calendar as seamlessly as possible with a wide feature set for
diff --git a/build.gradle.kts b/build.gradle.kts
index de9a5ccf5..4f054a085 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,64 +1,73 @@
+import org.gradle.api.tasks.wrapper.Wrapper.DistributionType.ALL
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
- java
-
//kotlin
kotlin("jvm")
kotlin("plugin.serialization")
- kotlin("plugin.spring") apply false
- id("org.jetbrains.kotlin.plugin.allopen") apply false
+ id("org.jetbrains.kotlin.plugin.allopen")
- //Other
+ // Spring
+ kotlin("plugin.spring")
id("org.springframework.boot") apply false
+ id("io.spring.dependency-management")
+
+ //Tooling
id("com.gorylenko.gradle-git-properties") apply false
id("com.google.cloud.tools.jib") apply false
}
buildscript {
+ val kotlinPoetVersion: String by properties
dependencies {
- classpath("com.squareup:kotlinpoet:1.7.2")
+ classpath("com.squareup:kotlinpoet:$kotlinPoetVersion")
}
}
allprojects {
//Project props
group = "org.dreamexposure.discal"
- version = "4.2.2"
+ version = "4.2.3"
description = "DisCal"
//Plugins
apply(plugin = "java")
apply(plugin = "kotlin")
+ apply(plugin = "io.spring.dependency-management")
- //Compiler nonsense
- java.sourceCompatibility = JavaVersion.VERSION_16
- java.targetCompatibility = JavaVersion.VERSION_16
-
- //Versions
+ // Versions --- found in gradle.properties
val kotlinVersion: String by properties
- val kotlinxSerializationVersion: String by properties
-
+ // Tool
+ val kotlinxCoroutinesReactorVersion: String by properties
+ val reactorKotlinExtensions: String by properties
+ // Discord
+ val discord4jVersion: String by properties
+ val discord4jStoresVersion: String by properties
+ val discordWebhookVersion: String by properties
+ // Spring
val springVersion: String by properties
-
- val googleCoreVersion: String by properties
- val googleCalendarVersion: String by properties
-
- val r2MysqlVersion: String by properties
- val r2PoolVersion: String by properties
-
- val nettyVersion: String by properties
- val reactorBomVersion: String by properties
-
- val slfVersion: String by properties
+ // Database
+ val flywayVersion: String by properties
+ val mikuR2dbcMySqlVersion: String by properties
+ val mySqlConnectorJava: String by properties
+ // Serialization
+ val kotlinxSerializationJsonVersion: String by properties
+ val jacksonVersion: String by properties
val jsonVersion: String by properties
- val okHttpVersion: String by properties
- val discordWebhookVersion: String by properties
+ // Google libs
+ val googleApiClientVersion: String by properties
+ val googleServicesCalendarVersion: String by properties
+ val googleOauthClientVersion: String by properties
+ // Various libs
+ val okhttpVersion: String by properties
val copyDownVersion: String by properties
+ val jsoupVersion: String by properties
repositories {
mavenCentral()
+ mavenLocal()
+ maven("https://repo.maven.apache.org/maven2/")
maven("https://kotlin.bintray.com/kotlinx")
maven("https://oss.sonatype.org/content/repositories/snapshots")
maven("https://repo.spring.io/milestone")
@@ -66,48 +75,50 @@ allprojects {
}
dependencies {
- //Boms
- implementation(platform("io.projectreactor:reactor-bom:$reactorBomVersion"))
-
- //Kotlin Deps
+ // Tools
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion")
- implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion")
-
- //Forced stuff
- //slf4j-api - Need to force this for logback to work. I dunno
- implementation("org.slf4j:slf4j-api:$slfVersion")
- //Netty - forced due to stores-redis:lettuce-core giving 4.1.38
- implementation("io.netty:netty-all:$nettyVersion")
- //Forcing reactor version
- implementation("io.projectreactor:reactor-core")
-
- //Google apis
- implementation("com.google.api-client:google-api-client:$googleCoreVersion")
- implementation("com.google.apis:google-api-services-calendar:$googleCalendarVersion")
- implementation("com.google.oauth-client:google-oauth-client-jetty:$googleCoreVersion") {
- exclude(group = "org.mortbay.jetty", module = "servlet-api")
- }
- //r2dbc
- implementation("dev.miku:r2dbc-mysql:$r2MysqlVersion") {
- exclude("io.netty", "*")
- exclude("io.projectreactor", "*")
- exclude("io.projectreactor.netty", "*")
- }
- implementation("io.r2dbc:r2dbc-pool:$r2PoolVersion")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinxCoroutinesReactorVersion")
+ implementation("io.projectreactor.kotlin:reactor-kotlin-extensions:$reactorKotlinExtensions")
+ // Discord
+ implementation("com.discord4j:discord4j-core:$discord4jVersion")
+ implementation("com.discord4j:stores-redis:$discord4jStoresVersion")
+ implementation("club.minnced:discord-webhooks:$discordWebhookVersion")
- implementation("org.json:json:$jsonVersion")
+ // Spring
+ implementation("org.springframework.boot:spring-boot-starter-data-jdbc:$springVersion")
+ implementation("org.springframework.boot:spring-boot-starter-data-r2dbc:$springVersion")
+ implementation("org.springframework.boot:spring-boot-starter-data-redis:$springVersion")
+ implementation("org.springframework.boot:spring-boot-starter-webflux:$springVersion")
+ implementation("org.springframework.boot:spring-boot-starter-cache:$springVersion")
- implementation("com.squareup.okhttp3:okhttp:$okHttpVersion")
+ // Database
+ implementation("dev.miku:r2dbc-mysql:$mikuR2dbcMySqlVersion")
+ implementation("mysql:mysql-connector-java:$mySqlConnectorJava")
- implementation("club.minnced:discord-webhooks:$discordWebhookVersion")
+ // Serialization
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationJsonVersion")
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
+ implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion")
+ implementation("org.json:json:$jsonVersion")
+
+ // Google libs
+ implementation("com.google.api-client:google-api-client:$googleApiClientVersion")
+ implementation("com.google.apis:google-api-services-calendar:$googleServicesCalendarVersion")
+ implementation("com.google.oauth-client:google-oauth-client-jetty:$googleOauthClientVersion") {
+ exclude(group = "org.mortbay.jetty", module = "servlet-api")
+ }
+ // Various Libs
+ implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
implementation("io.github.furstenheim:copy_down:$copyDownVersion")
+ implementation("org.jsoup:jsoup:$jsoupVersion")
+ }
- //Spring
- implementation("org.springframework.boot:spring-boot-starter-webflux:$springVersion")
- implementation("org.springframework.boot:spring-boot-starter-data-r2dbc:$springVersion")
+ java {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
}
kotlin {
@@ -127,9 +138,16 @@ subprojects {
withType {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
- jvmTarget = targetCompatibility
+ jvmTarget = java.targetCompatibility.majorVersion
}
}
}
}
+tasks {
+ wrapper {
+ distributionType = ALL
+ gradleVersion = "8.2.1"
+ }
+}
+
diff --git a/cam/build.gradle.kts b/cam/build.gradle.kts
index 8fdb0f878..33de04b4c 100644
--- a/cam/build.gradle.kts
+++ b/cam/build.gradle.kts
@@ -1,24 +1,19 @@
plugins {
+ // Kotlin
kotlin("plugin.serialization")
+ id("org.jetbrains.kotlin.plugin.allopen")
+
+ // Spring
kotlin("plugin.spring")
id("org.springframework.boot")
- id("org.jetbrains.kotlin.plugin.allopen")
+ id("io.spring.dependency-management")
+
+ // Tooling
id("com.google.cloud.tools.jib")
}
-val springSessionVersion: String by properties
-val springR2Version: String by properties
-val jacksonKotlinModVersion: String by properties
-
dependencies {
api(project(":core"))
-
- //Spring libs
- implementation("org.springframework.session:spring-session-data-redis:$springSessionVersion")
- implementation("org.springframework:spring-r2dbc:$springR2Version")
-
- //jackson for kotlin
- implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonKotlinModVersion")
}
kotlin {
@@ -30,14 +25,13 @@ kotlin {
}
jib {
- var imageVersion = version.toString()
- if (imageVersion.contains("SNAPSHOT")) imageVersion = "latest"
+ to {
+ image = "rg.nl-ams.scw.cloud/dreamexposure/discal-cam"
+ tags = mutableSetOf("latest", version.toString())
+ }
- to.image = "rg.nl-ams.scw.cloud/dreamexposure/discal-cam:$imageVersion"
val baseImage: String by properties
from.image = baseImage
-
- container.creationTime = "USE_CURRENT_TIMESTAMP"
}
tasks {
diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/Cam.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/Cam.kt
index c8f247658..4b4f42565 100644
--- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/Cam.kt
+++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/Cam.kt
@@ -1,45 +1,28 @@
package org.dreamexposure.discal.cam
+import jakarta.annotation.PreDestroy
import org.dreamexposure.discal.Application
-import org.dreamexposure.discal.cam.google.GoogleInternalAuthHandler
-import org.dreamexposure.discal.core.`object`.BotSettings
-import org.dreamexposure.discal.core.database.DatabaseManager
+import org.dreamexposure.discal.core.config.Config
import org.dreamexposure.discal.core.logger.LOGGER
import org.dreamexposure.discal.core.utils.GlobalVal
import org.springframework.boot.builder.SpringApplicationBuilder
import org.springframework.stereotype.Component
-import java.io.FileReader
-import java.util.*
-import javax.annotation.PreDestroy
import kotlin.system.exitProcess
@Component
class Cam {
@PreDestroy
- fun onShutdown() {
- LOGGER.info(GlobalVal.STATUS, "CAM shutting down.")
- DatabaseManager.disconnectFromMySQL()
- }
+ fun onShutdown() = LOGGER.info(GlobalVal.STATUS, "CAM shutting down.")
companion object {
@JvmStatic
fun main(args: Array) {
- //Get settings
- val p = Properties()
- p.load(FileReader("settings.properties"))
- BotSettings.init(p)
-
- //Handle generating new google auth credentials for discal accounts
- if (args.size > 1 && args[0].equals("-forceNewGoogleAuth", true)) {
- //This will automatically kill this instance once finished
- GoogleInternalAuthHandler.requestCode(args[1].toInt()).subscribe()
- }
+ Config.init()
//Start up spring
try {
SpringApplicationBuilder(Application::class.java)
- .profiles(BotSettings.PROFILE.get())
.build()
.run(*args)
LOGGER.info(GlobalVal.STATUS, "CAM is now online")
diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/business/OauthStateService.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/business/OauthStateService.kt
new file mode 100644
index 000000000..5352a697f
--- /dev/null
+++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/business/OauthStateService.kt
@@ -0,0 +1,19 @@
+package org.dreamexposure.discal.cam.business
+
+import org.dreamexposure.discal.OauthStateCache
+import org.dreamexposure.discal.core.crypto.KeyGenerator
+import org.springframework.stereotype.Component
+
+@Component
+class OauthStateService(
+ private val stateCache: OauthStateCache,
+) {
+ suspend fun generateState(): String {
+ val state = KeyGenerator.csRandomAlphaNumericString(64)
+ stateCache.put(state, state)
+
+ return state
+ }
+
+ suspend fun validateState(state: String) = stateCache.getAndRemove(state) != null
+}
diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/business/cronjob/HeartbeatCronJob.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/business/cronjob/HeartbeatCronJob.kt
new file mode 100644
index 000000000..f8dcd002d
--- /dev/null
+++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/business/cronjob/HeartbeatCronJob.kt
@@ -0,0 +1,55 @@
+package org.dreamexposure.discal.cam.business.cronjob
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import kotlinx.coroutines.reactor.mono
+import okhttp3.Request
+import okhttp3.RequestBody.Companion.toRequestBody
+import okhttp3.Response
+import org.dreamexposure.discal.core.config.Config
+import org.dreamexposure.discal.core.extensions.asSeconds
+import org.dreamexposure.discal.core.logger.LOGGER
+import org.dreamexposure.discal.core.`object`.network.discal.InstanceData
+import org.dreamexposure.discal.core.`object`.rest.HeartbeatRequest
+import org.dreamexposure.discal.core.`object`.rest.HeartbeatType
+import org.dreamexposure.discal.core.utils.GlobalVal
+import org.dreamexposure.discal.core.utils.GlobalVal.HTTP_CLIENT
+import org.dreamexposure.discal.core.utils.GlobalVal.JSON
+import org.springframework.boot.ApplicationArguments
+import org.springframework.boot.ApplicationRunner
+import org.springframework.stereotype.Component
+import reactor.core.publisher.Flux
+import reactor.core.publisher.Mono
+import reactor.core.scheduler.Schedulers
+
+@Component
+class HeartbeatCronJob(
+ private val objectMapper: ObjectMapper,
+): ApplicationRunner {
+ private final val apiUrl = Config.URL_API.getString()
+
+ override fun run(args: ApplicationArguments?) {
+ Flux.interval(Config.HEARTBEAT_INTERVAL.getLong().asSeconds())
+ .flatMap { heartbeat() }
+ .doOnError { LOGGER.error(GlobalVal.DEFAULT, "[Heartbeat] Failed to heartbeat", it) }
+ .onErrorResume { Mono.empty() }
+ .subscribe()
+ }
+
+ private fun heartbeat() = mono {
+ val requestBody = HeartbeatRequest(HeartbeatType.CAM, instanceData = InstanceData())
+
+ val request = Request.Builder()
+ .url("$apiUrl/v2/status/heartbeat")
+ .post(objectMapper.writeValueAsString(requestBody).toRequestBody(JSON))
+ .header("Authorization", Config.SECRET_DISCAL_API_KEY.getString())
+ .header("Content-Type", "application/json")
+ .build()
+
+ Mono.fromCallable(HTTP_CLIENT.newCall(request)::execute)
+ .map(Response::close)
+ .subscribeOn(Schedulers.boundedElastic())
+ .doOnError { LOGGER.error(GlobalVal.DEFAULT, "[Heartbeat] Failed to heartbeat", it) }
+ .onErrorResume { Mono.empty() }
+ .subscribe()
+ }
+}
diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/business/cronjob/SessionCronJob.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/business/cronjob/SessionCronJob.kt
new file mode 100644
index 000000000..84f1905c7
--- /dev/null
+++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/business/cronjob/SessionCronJob.kt
@@ -0,0 +1,31 @@
+package org.dreamexposure.discal.cam.business.cronjob
+
+import kotlinx.coroutines.reactor.mono
+import org.dreamexposure.discal.core.business.SessionService
+import org.dreamexposure.discal.core.logger.LOGGER
+import org.dreamexposure.discal.core.utils.GlobalVal
+import org.springframework.boot.ApplicationArguments
+import org.springframework.boot.ApplicationRunner
+import org.springframework.stereotype.Component
+import reactor.core.publisher.Flux
+import reactor.core.publisher.Mono
+import java.time.Duration
+
+@Component
+class SessionCronJob(
+ val sessionService: SessionService,
+) : ApplicationRunner {
+ override fun run(args: ApplicationArguments?) {
+ Flux.interval(Duration.ofHours(1))
+ .flatMap { justDoIt() }
+ .doOnError { LOGGER.error(GlobalVal.DEFAULT, "Session cronjob error", it) }
+ .onErrorResume { Mono.empty() }
+ .subscribe()
+ }
+
+ private fun justDoIt() = mono {
+ LOGGER.debug("Running expired session purge job")
+
+ sessionService.deleteExpiredSessions()
+ }
+}
diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/discord/DiscordOauthHandler.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/discord/DiscordOauthHandler.kt
index 583013344..dad3ec25e 100644
--- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/discord/DiscordOauthHandler.kt
+++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/discord/DiscordOauthHandler.kt
@@ -1,117 +1,116 @@
package org.dreamexposure.discal.cam.discord
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.readValue
+import kotlinx.coroutines.reactor.awaitSingle
import okhttp3.FormBody
import okhttp3.Request
import org.dreamexposure.discal.cam.json.discord.AccessTokenResponse
import org.dreamexposure.discal.cam.json.discord.AuthorizationInfo
+import org.dreamexposure.discal.core.config.Config
import org.dreamexposure.discal.core.exceptions.AuthenticationException
-import org.dreamexposure.discal.core.`object`.BotSettings
import org.dreamexposure.discal.core.utils.GlobalVal
import org.dreamexposure.discal.core.utils.GlobalVal.HTTP_CLIENT
-import org.dreamexposure.discal.core.utils.GlobalVal.JSON_FORMAT
+import org.springframework.stereotype.Component
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
-import java.net.URLEncoder
-import java.nio.charset.Charset
-
-object DiscordOauthHandler {
- private val redirectUrl = URLEncoder.encode(BotSettings.REDIR_URL.get(), Charset.defaultCharset())
- private const val CDN_URL = "https://cdn.discordapp.com"
-
- fun doTokenExchange(code: String): Mono {
- return Mono.fromCallable {
- val body = FormBody.Builder()
- .addEncoded("client_id", BotSettings.ID.get())
- .addEncoded("client_secret", BotSettings.SECRET.get())
- .addEncoded("grant_type", "authorization_code")
- .addEncoded("code", code)
- .addEncoded("redirect_uri", redirectUrl)
- .build()
-
- val tokenExchangeRequest = Request.Builder()
- .url("${GlobalVal.discordApiUrl}/oauth2/token")
- .post(body)
- .header("Content-Type", "application/x-www-form-urlencoded")
- .build()
-
- HTTP_CLIENT.newCall(tokenExchangeRequest).execute()
- }.subscribeOn(Schedulers.boundedElastic()).flatMap { response ->
- if (response.isSuccessful) {
- //Transform body into our object
- val responseBody = JSON_FORMAT.decodeFromString(AccessTokenResponse.serializer(),
- response.body!!.string())
- // Close body to avoid mem leak
- response.body?.close()
-
- Mono.just(responseBody)
- } else {
- Mono.error(AuthenticationException("Discord authorization grant error"))
- }
+
+@Component
+class DiscordOauthHandler(
+ private val objectMapper: ObjectMapper,
+) {
+ private val cdnUrl = "https://cdn.discordapp.com"
+ private val redirectUrl = Config.URL_DISCORD_REDIRECT.getString()
+ private val clientId = Config.DISCORD_APP_ID.getString()
+ private val clientSecret = Config.SECRET_CLIENT_SECRET.getString()
+
+ suspend fun doTokenExchange(code: String): AccessTokenResponse {
+ val body = FormBody.Builder()
+ .addEncoded("client_id", clientId)
+ .addEncoded("client_secret", clientSecret)
+ .addEncoded("grant_type", "authorization_code")
+ .addEncoded("code", code)
+ .addEncoded("redirect_uri", redirectUrl)
+ .build()
+ val tokenExchangeRequest = Request.Builder()
+ .url("${GlobalVal.discordApiUrl}/oauth2/token")
+ .post(body)
+ .header("Content-Type", "application/x-www-form-urlencoded")
+ .build()
+
+ val response = Mono.fromCallable(HTTP_CLIENT.newCall(tokenExchangeRequest)::execute)
+ .subscribeOn(Schedulers.boundedElastic())
+ .awaitSingle()
+
+ if (response.isSuccessful) {
+ val responseBody = objectMapper.readValue(response.body!!.string())
+ response.close()
+
+ return responseBody
+ } else {
+ throw AuthenticationException("Discord authorization grant error")
}
}
- fun doTokenRefresh(refreshToken: String): Mono {
- return Mono.fromCallable {
- val body = FormBody.Builder()
- .addEncoded("client_id", BotSettings.ID.get())
- .addEncoded("client_secret", BotSettings.SECRET.get())
- .addEncoded("grant_type", "refresh_token")
- .addEncoded("refresh_token", refreshToken)
- .build()
-
- val tokenExchangeRequest = Request.Builder()
- .url("${GlobalVal.discordApiUrl}/oauth2/token")
- .post(body)
- .header("Content-Type", "application/x-www-form-urlencoded")
- .build()
-
- HTTP_CLIENT.newCall(tokenExchangeRequest).execute()
- }.subscribeOn(Schedulers.boundedElastic()).flatMap { response ->
- if (response.isSuccessful) {
- //Transform body into our object
- val responseBody = JSON_FORMAT.decodeFromString(AccessTokenResponse.serializer(),
- response.body!!.string())
- // Close body to avoid mem leak
- response.body?.close()
-
- Mono.just(responseBody)
- } else {
- Mono.error(AuthenticationException("Discord refresh token error"))
- }
+ suspend fun doTokenRefresh(refreshToken: String): AccessTokenResponse {
+ val body = FormBody.Builder()
+ .addEncoded("client_id", clientId)
+ .addEncoded("client_secret", clientSecret)
+ .addEncoded("grant_type", "refresh_token")
+ .addEncoded("refresh_token", refreshToken)
+ .build()
+
+ val tokenExchangeRequest = Request.Builder()
+ .url("${GlobalVal.discordApiUrl}/oauth2/token")
+ .post(body)
+ .header("Content-Type", "application/x-www-form-urlencoded")
+ .build()
+
+ val response = Mono.fromCallable(HTTP_CLIENT.newCall(tokenExchangeRequest)::execute)
+ .subscribeOn(Schedulers.boundedElastic())
+ .awaitSingle()
+
+ if (response.isSuccessful) {
+ val responseBody = objectMapper.readValue(response.body!!.string())
+ response.close()
+
+ return responseBody
+ } else {
+ throw AuthenticationException("Discord refresh token error")
}
}
- fun getOauthInfo(accessToken: String): Mono {
- return Mono.fromCallable {
- val request = Request.Builder()
- .url("${GlobalVal.discordApiUrl}/oauth2/@me")
- .get()
- .header("Authorization", "Bearer $accessToken")
- .build()
-
- HTTP_CLIENT.newCall(request).execute()
- }.subscribeOn(Schedulers.boundedElastic()).flatMap { response ->
- if (response.isSuccessful) {
- //Transform body into our object
- var responseBody = JSON_FORMAT.decodeFromString(AuthorizationInfo.serializer(),
- response.body!!.string())
-
- //Convert avatar hash to full URL
- val avatar = if (responseBody.user!!.avatar != null) {
- val userId = responseBody.user!!.id.asString()
- val avatarHash = responseBody.user!!.avatar
- "$CDN_URL/avatars/$userId/$avatarHash.png"
- } else {
- // No avatar present, get discord's default user avatar
- val discrim = responseBody.user!!.discriminator
- "$CDN_URL/embed/avatars/${discrim.toInt() % 5}.png"
- }
- responseBody = responseBody.copy(user = responseBody.user!!.copy(avatar = avatar))
-
- Mono.just(responseBody)
+
+ suspend fun getOauthInfo(accessToken: String): AuthorizationInfo {
+ val request = Request.Builder()
+ .url("${GlobalVal.discordApiUrl}/oauth2/@me")
+ .get()
+ .header("Authorization", "Bearer $accessToken")
+ .build()
+
+ val response = Mono.fromCallable(HTTP_CLIENT.newCall(request)::execute)
+ .subscribeOn(Schedulers.boundedElastic())
+ .awaitSingle()
+
+ if (response.isSuccessful) {
+ var responseBody = objectMapper.readValue(response.body!!.string())
+ response.close()
+
+ //Convert avatar hash to full URL
+ val avatar = if (responseBody.user!!.avatar != null) {
+ val userId = responseBody.user!!.id.asString()
+ val avatarHash = responseBody.user!!.avatar
+ "$cdnUrl/avatars/$userId/$avatarHash.png"
} else {
- Mono.error(AuthenticationException("Discord auth info error"))
+ // No avatar present, get discord's default user avatar
+ val discrim = responseBody.user!!.discriminator
+ "$cdnUrl/embed/avatars/${discrim.toInt() % 5}.png"
}
+ responseBody = responseBody.copy(user = responseBody.user!!.copy(avatar = avatar))
+
+ return responseBody
+ } else {
+ throw AuthenticationException("Discord auth info error")
}
}
}
diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/endpoints/v1/GetEndpoint.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/endpoints/v1/GetEndpoint.kt
index f36298dba..a21e653cf 100644
--- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/endpoints/v1/GetEndpoint.kt
+++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/endpoints/v1/GetEndpoint.kt
@@ -1,36 +1,23 @@
package org.dreamexposure.discal.cam.endpoints.v1
import discord4j.common.util.Snowflake
-import org.dreamexposure.discal.cam.google.GoogleAuth
-import org.dreamexposure.discal.core.`object`.network.discal.CredentialData
+import org.dreamexposure.discal.cam.managers.CalendarAuthManager
import org.dreamexposure.discal.core.annotations.Authentication
-import org.dreamexposure.discal.core.database.DatabaseManager
import org.dreamexposure.discal.core.enums.calendar.CalendarHost
+import org.dreamexposure.discal.core.`object`.network.discal.CredentialData
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
-import reactor.core.publisher.Mono
@RestController
@RequestMapping("/v1/")
-class GetEndpoint {
-
+class GetEndpoint(
+ private val calendarAuthManager: CalendarAuthManager,
+) {
@Authentication(access = Authentication.AccessLevel.ADMIN)
@GetMapping("token", produces = ["application/json"])
- fun get(@RequestParam host: CalendarHost, @RequestParam id: Int, @RequestParam guild: Snowflake?): Mono {
- return Mono.defer {
- when (host) {
- CalendarHost.GOOGLE -> {
- if (guild == null) {
- // Internal (owned by DisCal, should never go bad)
- GoogleAuth.requestNewAccessToken(id)
- } else {
- // External (owned by user)
- DatabaseManager.getCalendar(guild, id).flatMap(GoogleAuth::requestNewAccessToken)
- }
- }
- }
- }
+ suspend fun get(@RequestParam host: CalendarHost, @RequestParam id: Int, @RequestParam guild: Snowflake?): CredentialData? {
+ return calendarAuthManager.getCredentialData(host, id, guild)
}
}
diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/endpoints/v1/oauth2/DiscordOauthEndpoint.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/endpoints/v1/oauth2/DiscordOauthEndpoint.kt
index 53170a275..54103efa5 100644
--- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/endpoints/v1/oauth2/DiscordOauthEndpoint.kt
+++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/endpoints/v1/oauth2/DiscordOauthEndpoint.kt
@@ -1,78 +1,34 @@
package org.dreamexposure.discal.cam.endpoints.v1.oauth2
-import org.dreamexposure.discal.cam.discord.DiscordOauthHandler
import org.dreamexposure.discal.cam.json.discal.LoginResponse
import org.dreamexposure.discal.cam.json.discal.TokenRequest
import org.dreamexposure.discal.cam.json.discal.TokenResponse
-import org.dreamexposure.discal.cam.service.StateService
-import org.dreamexposure.discal.core.`object`.BotSettings.ID
-import org.dreamexposure.discal.core.`object`.BotSettings.REDIR_URL
-import org.dreamexposure.discal.core.`object`.WebSession
+import org.dreamexposure.discal.cam.managers.DiscordOauthManager
import org.dreamexposure.discal.core.annotations.Authentication
-import org.dreamexposure.discal.core.crypto.KeyGenerator
-import org.dreamexposure.discal.core.database.DatabaseManager
-import org.dreamexposure.discal.core.utils.GlobalVal
-import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.*
-import org.springframework.web.server.ResponseStatusException
-import reactor.core.publisher.Mono
-import java.net.URLEncoder
-import java.nio.charset.Charset.defaultCharset
@RestController
@RequestMapping("/oauth2/discord/")
-class DiscordOauthEndpoint(private val stateService: StateService) {
- private final val scopes = URLEncoder.encode("identify guilds", defaultCharset())
- private final val redirectUrl = URLEncoder.encode(REDIR_URL.get(), defaultCharset())
- private final val oauthLinkWithoutState =
- "${GlobalVal.discordApiUrl}/oauth2/authorize" +
- "?client_id=${ID.get()}" +
- "&redirect_uri=$redirectUrl" +
- "&response_type=code" +
- "&scope=$scopes" +
- "&prompt=none" // Skip consent screen if user has already authorized these scopes before
+class DiscordOauthEndpoint(
+ private val discordOauthManager: DiscordOauthManager,
+) {
@GetMapping("login")
@Authentication(access = Authentication.AccessLevel.PUBLIC)
- fun login(): Mono {
- val state = stateService.generateState()
-
- val link = "$oauthLinkWithoutState&state=$state"
-
- return Mono.just(LoginResponse(link))
+ suspend fun login(): LoginResponse {
+ val link = discordOauthManager.getOauthLinkForLogin()
+ return LoginResponse(link)
}
@GetMapping("logout")
@Authentication(access = Authentication.AccessLevel.WRITE)
- fun logout(@RequestHeader("Authorization") token: String): Mono {
- return DatabaseManager.deleteSession(token).then()
+ suspend fun logout(@RequestHeader("Authorization") token: String) {
+ discordOauthManager.handleLogout(token)
}
@PostMapping("code")
@Authentication(access = Authentication.AccessLevel.PUBLIC)
- fun token(@RequestBody body: TokenRequest): Mono {
- // Validate state
- if (!stateService.validateState(body.state)) {
- // State invalid - 400
- return Mono.error(ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid state"))
- }
-
- return DiscordOauthHandler.doTokenExchange(body.code).flatMap { dTokens ->
- // request current user info
- DiscordOauthHandler.getOauthInfo(dTokens.accessToken).flatMap { authInfo ->
- val apiToken = KeyGenerator.csRandomAlphaNumericString(64)
-
- val session = WebSession(
- apiToken,
- authInfo.user!!.id,
- accessToken = dTokens.accessToken,
- refreshToken = dTokens.refreshToken
- )
-
- // Save session data then return response
- DatabaseManager.removeAndInsertSessionData(session)
- .thenReturn(TokenResponse(session.token, session.expiresAt, authInfo.user))
- }
- }
+ suspend fun token(@RequestBody body: TokenRequest): TokenResponse {
+ return discordOauthManager.handleCodeExchange(body.state, body.code)
}
}
diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/google/GoogleAuth.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/google/GoogleAuth.kt
index 3c0db95d8..3b34a6505 100644
--- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/google/GoogleAuth.kt
+++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/google/GoogleAuth.kt
@@ -1,134 +1,121 @@
package org.dreamexposure.discal.cam.google
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.readValue
import com.google.api.client.http.HttpStatusCodes.STATUS_CODE_BAD_REQUEST
import com.google.api.client.http.HttpStatusCodes.STATUS_CODE_OK
+import kotlinx.coroutines.reactor.awaitSingle
import okhttp3.FormBody
import okhttp3.Request
import org.dreamexposure.discal.cam.json.google.ErrorData
import org.dreamexposure.discal.cam.json.google.RefreshData
-import org.dreamexposure.discal.core.`object`.BotSettings
-import org.dreamexposure.discal.core.`object`.calendar.CalendarData
-import org.dreamexposure.discal.core.`object`.google.ClientData
-import org.dreamexposure.discal.core.`object`.network.discal.CredentialData
+import org.dreamexposure.discal.core.business.CalendarService
+import org.dreamexposure.discal.core.business.CredentialService
+import org.dreamexposure.discal.core.config.Config
import org.dreamexposure.discal.core.crypto.AESEncryption
-import org.dreamexposure.discal.core.database.DatabaseManager
-import org.dreamexposure.discal.core.entities.google.DisCalGoogleCredential
import org.dreamexposure.discal.core.exceptions.AccessRevokedException
import org.dreamexposure.discal.core.exceptions.EmptyNotAllowedException
import org.dreamexposure.discal.core.exceptions.NotFoundException
+import org.dreamexposure.discal.core.extensions.isExpiredTtl
import org.dreamexposure.discal.core.logger.LOGGER
+import org.dreamexposure.discal.core.`object`.network.discal.CredentialData
+import org.dreamexposure.discal.core.`object`.new.Calendar
import org.dreamexposure.discal.core.utils.GlobalVal.DEFAULT
import org.dreamexposure.discal.core.utils.GlobalVal.HTTP_CLIENT
-import org.dreamexposure.discal.core.utils.GlobalVal.JSON_FORMAT
-import reactor.core.publisher.Flux
+import org.springframework.stereotype.Component
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
import java.time.Instant
-import kotlin.system.exitProcess
-
-@Suppress("BlockingMethodInNonBlockingContext")
-object GoogleAuth {
- private val clientData = ClientData(BotSettings.GOOGLE_CLIENT_ID.get(), BotSettings.GOOGLE_CLIENT_SECRET.get())
- private val CREDENTIALS: Flux
-
- init {
- val credCount = BotSettings.CREDENTIALS_COUNT.get().toInt()
- CREDENTIALS = Flux.range(0, credCount)
- .flatMap(DatabaseManager::getCredentialData)
- .map(::DisCalGoogleCredential)
- .doOnError { exitProcess(1) }
- .cache()
- }
- fun requestNewAccessToken(calendarData: CalendarData): Mono {
- return Mono.just(AESEncryption(calendarData.privateKey)).flatMap { aes ->
- if (!calendarData.expired()) {
- return@flatMap aes.decrypt(calendarData.encryptedAccessToken)
- .map { CredentialData(it, calendarData.expiresAt) }
- }
-
- aes.decrypt(calendarData.encryptedRefreshToken)
- .flatMap(this::doAccessTokenRequest)
- .flatMap { data ->
- //calendarData.encryptedAccessToken = aes.encrypt(data.accessToken)
- calendarData.expiresAt = data.validUntil
+@Component
+class GoogleAuth(
+ private val credentialService: CredentialService,
+ private val calendarService: CalendarService,
+ private val objectMapper: ObjectMapper,
+) {
+ private final val aes: AESEncryption = AESEncryption(Config.SECRET_GOOGLE_CREDENTIAL_KEY.getString())
- aes.encrypt(data.accessToken)
- .doOnNext { calendarData.encryptedAccessToken = it }
- .then(DatabaseManager.updateCalendar(calendarData).thenReturn(data))
- }
+ suspend fun requestNewAccessToken(calendar: Calendar): CredentialData? {
+ val aes = AESEncryption(calendar.secrets.privateKey)
+ if (!calendar.secrets.expiresAt.isExpiredTtl()) {
+ return aes.decrypt(calendar.secrets.encryptedAccessToken)
+ .map { CredentialData(it, calendar.secrets.expiresAt) }
+ .awaitSingle()
}
+
+ val refreshToken = aes.decrypt(calendar.secrets.encryptedRefreshToken).awaitSingle()
+ val refreshedCredential = doAccessTokenRequest(refreshToken) ?: return null
+
+ calendar.secrets.expiresAt = refreshedCredential.validUntil.minusSeconds(60) // Add a minute of wiggle room
+ calendar.secrets.encryptedAccessToken = aes.encrypt(refreshedCredential.accessToken).awaitSingle()
+
+ calendarService.updateCalendar(calendar)
+
+ return refreshedCredential
}
- fun requestNewAccessToken(credentialId: Int): Mono {
- return CREDENTIALS
- .filter { it.credentialData.credentialNumber == credentialId }
- .next()
- .switchIfEmpty(Mono.error(NotFoundException()))
- .flatMap { credential ->
- if (!credential.expired()) {
- return@flatMap credential.getAccessToken()
- .map { CredentialData(it, credential.credentialData.expiresAt) }
- }
-
- credential.getRefreshToken()
- .flatMap(this::doAccessTokenRequest)
- .flatMap { credential.setAccessToken(it.accessToken).thenReturn(it) }
- .doOnNext { credential.credentialData.expiresAt = it.validUntil }
- .flatMap { DatabaseManager.updateCredentialData(credential.credentialData).thenReturn(it) }
- }.switchIfEmpty(Mono.error(EmptyNotAllowedException()))
+ suspend fun requestNewAccessToken(credentialId: Int): CredentialData {
+ val credential = credentialService.getCredential(credentialId) ?: throw NotFoundException()
+ if (!credential.expiresAt.isExpiredTtl()) {
+ val accessToken = aes.decrypt(credential.encryptedAccessToken).awaitSingle()
+ return CredentialData(accessToken, credential.expiresAt)
+ }
+
+ val refreshToken = aes.decrypt(credential.encryptedRefreshToken).awaitSingle()
+ val refreshedCredentialData = doAccessTokenRequest(refreshToken) ?: throw EmptyNotAllowedException()
+ credential.encryptedAccessToken = aes.encrypt(refreshedCredentialData.accessToken).awaitSingle()
+ credential.expiresAt = refreshedCredentialData.validUntil.minusSeconds(60) // Add a minute of wiggle room
+ credentialService.updateCredential(credential)
+ return refreshedCredentialData
}
- private fun doAccessTokenRequest(refreshToken: String): Mono {
- return Mono.fromCallable {
- val body = FormBody.Builder()
- .addEncoded("client_id", clientData.clientId)
- .addEncoded("client_secret", clientData.clientSecret)
- .addEncoded("refresh_token", refreshToken)
- .addEncoded("grant_type", "refresh_token")
- .build()
-
- val request = Request.Builder()
- .url("https://www.googleapis.com/oauth2/v4/token")
- .post(body)
- .header("Content-Type", "application/x-www-form-urlencoded")
- .build()
-
- HTTP_CLIENT.newCall(request).execute()
- }.subscribeOn(Schedulers.boundedElastic()).flatMap { response ->
- when (response.code) {
- STATUS_CODE_OK -> {
- val body = JSON_FORMAT.decodeFromString(RefreshData.serializer(), response.body!!.string())
- response.body?.close()
- response.close()
-
- Mono.just(CredentialData(body.accessToken, Instant.now().plusSeconds(body.expiresIn.toLong())))
- }
- STATUS_CODE_BAD_REQUEST -> {
- val body = JSON_FORMAT.decodeFromString(ErrorData.serializer(), response.body!!.string())
- response.body?.close()
- response.close()
-
- LOGGER.error("[Google] Access Token Request: $body")
-
- if (body.error == "invalid_grant") {
- LOGGER.debug(DEFAULT, "[Google] Access to resource has been revoked")
- Mono.error(AccessRevokedException())
- } else {
- LOGGER.debug(DEFAULT, "[Google] Error requesting new access token | ${response.code} | ${response.message} | $body")
- Mono.empty()
- }
- }
- else -> {
- // Failed to get OK. Send debug info
- LOGGER.debug(DEFAULT, "[Google] Error requesting new access token | ${response.code} " +
- "| ${response.message} | ${response.body?.string()}")
- response.body?.close()
- response.close()
- Mono.empty()
+ private suspend fun doAccessTokenRequest(refreshToken: String): CredentialData? {
+ val requestFormBody = FormBody.Builder()
+ .addEncoded("client_id", Config.SECRET_GOOGLE_CLIENT_ID.getString())
+ .addEncoded("client_secret", Config.SECRET_GOOGLE_CLIENT_SECRET.getString())
+ .addEncoded("refresh_token", refreshToken)
+ .addEncoded("grant_type", "refresh_token")
+ .build()
+ val request = Request.Builder()
+ .url("https://www.googleapis.com/oauth2/v4/token")
+ .post(requestFormBody)
+ .header("Content-Type", "application/x-www-form-urlencoded")
+ .build()
+
+
+ val response = Mono.fromCallable(HTTP_CLIENT.newCall(request)::execute)
+ .subscribeOn(Schedulers.boundedElastic())
+ .awaitSingle()
+
+ return when (response.code) {
+ STATUS_CODE_OK -> {
+ val body = objectMapper.readValue(response.body!!.string())
+ response.close()
+
+ CredentialData(body.accessToken, Instant.now().plusSeconds(body.expiresIn.toLong()))
+ }
+ STATUS_CODE_BAD_REQUEST -> {
+ val body = objectMapper.readValue(response.body!!.string())
+ response.close()
+
+ LOGGER.error("[Google] Access Token Request: $body")
+
+ if (body.error == "invalid_grant") {
+ LOGGER.debug(DEFAULT, "[Google] Access to resource has been revoked")
+ throw AccessRevokedException()
+ } else {
+ LOGGER.error(DEFAULT, "[Google] Error requesting new access token | ${response.code} | ${response.message} | $body")
+ null
}
}
+ else -> {
+ // Failed to get OK. Send error info
+ LOGGER.error(DEFAULT, "[Google] Error requesting new access token | ${response.code} ${response.message} | ${response.body?.string()}")
+ response.close()
+
+ null
+ }
}
}
}
diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/google/GoogleInternalAuthHandler.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/google/GoogleInternalAuthHandler.kt
deleted file mode 100644
index 2bbd4dde2..000000000
--- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/google/GoogleInternalAuthHandler.kt
+++ /dev/null
@@ -1,120 +0,0 @@
-package org.dreamexposure.discal.cam.google
-
-import org.dreamexposure.discal.core.`object`.BotSettings
-import org.dreamexposure.discal.core.`object`.google.GoogleCredentialData
-import org.dreamexposure.discal.core.`object`.google.InternalGoogleAuthPoll
-import org.dreamexposure.discal.core.crypto.AESEncryption
-import org.dreamexposure.discal.core.database.DatabaseManager
-import org.dreamexposure.discal.core.exceptions.EmptyNotAllowedException
-import org.dreamexposure.discal.core.exceptions.google.GoogleAuthCancelException
-import org.dreamexposure.discal.core.logger.LOGGER
-import org.dreamexposure.discal.core.utils.GlobalVal
-import org.dreamexposure.discal.core.wrapper.google.GoogleAuthWrapper
-import org.json.JSONObject
-import reactor.core.publisher.Mono
-import reactor.function.TupleUtils
-import java.time.Instant
-
-@Suppress("BlockingMethodInNonBlockingContext")
-object GoogleInternalAuthHandler {
- fun requestCode(credNumber: Int): Mono {
- return GoogleAuthWrapper.requestDeviceCode().flatMap { response ->
- val responseBody = response.body!!.string()
- response.body?.close()
- response.close()
-
- if (response.code == GlobalVal.STATUS_SUCCESS) {
- val codeResponse = JSONObject(responseBody)
-
- val url = codeResponse.getString("verification_url")
- val code = codeResponse.getString("user_code")
- LOGGER.debug(GlobalVal.DEFAULT, "[!GDC!] DisCal Google Cred Auth $credNumber", "$url | $code")
-
- val poll = InternalGoogleAuthPoll(
- credNumber,
- interval = codeResponse.getInt("interval"),
- expiresIn = codeResponse.getInt("expires_in"),
- remainingSeconds = codeResponse.getInt("expires_in"),
- deviceCode = codeResponse.getString("device_code"),
- ) { this.pollForAuth(it as InternalGoogleAuthPoll) }
-
- GoogleAuthWrapper.scheduleOAuthPoll(poll)
- } else {
- LOGGER.debug(GlobalVal.DEFAULT, "Error request access token Status code: ${response.code} | ${response.message}" +
- " | $responseBody")
-
- Mono.empty()
- }
- }
- }
-
- private fun pollForAuth(poll: InternalGoogleAuthPoll): Mono {
- return GoogleAuthWrapper.requestPollResponse(poll).flatMap { response ->
- val responseBody = response.body!!.string()
- response.body?.close()
- response.close()
-
- when (response.code) {
- GlobalVal.STATUS_FORBIDDEN -> {
- //Handle access denied
- LOGGER.debug(GlobalVal.DEFAULT, "[!GDC!] Access denied for credential: ${poll.credNumber}")
-
- Mono.error(GoogleAuthCancelException())
- }
- GlobalVal.STATUS_BAD_REQUEST, GlobalVal.STATUS_PRECONDITION_REQUIRED -> {
- //See if auth is pending, if so, just reschedule.
-
- val aprError = JSONObject(responseBody)
- when {
- aprError.optString("error").equals("authorization_pending", true) -> {
- //Response pending
- Mono.empty()
- }
- aprError.optString("error").equals("expired_token", true) -> {
- //Token expired, auth is cancelled
- LOGGER.debug(GlobalVal.DEFAULT, "[!GDC!] token expired.")
-
- Mono.error(GoogleAuthCancelException())
- }
- else -> {
- LOGGER.debug(GlobalVal.DEFAULT, "[!GDC!] Poll Failure! Status code: ${response.code}" +
- " | ${response.message} | $responseBody")
-
- Mono.error(GoogleAuthCancelException())
- }
- }
- }
- GlobalVal.STATUS_RATE_LIMITED -> {
- //We got rate limited... oops. Let's just poll half as often...
- poll.interval = poll.interval * 2
-
- Mono.empty()
- }
- GlobalVal.STATUS_SUCCESS -> {
- //Access granted, save credentials...
- val aprGrant = JSONObject(responseBody)
- val aes = AESEncryption(BotSettings.CREDENTIALS_KEY.get())
-
- val refreshMono = aes.encrypt(aprGrant.getString("refresh_token"))
- val accessMono = aes.encrypt(aprGrant.getString("access_token"))
-
- Mono.zip(refreshMono, accessMono).flatMap(TupleUtils.function { refresh, access ->
- val expiresAt = Instant.now().plusSeconds(aprGrant.getLong("expires_in"))
-
- val creds = GoogleCredentialData(poll.credNumber, refresh, access, expiresAt)
-
- DatabaseManager.updateCredentialData(creds)
- .then(Mono.error(GoogleAuthCancelException()))
- }).onErrorResume(EmptyNotAllowedException::class.java) { Mono.error(GoogleAuthCancelException()) }
- }
- else -> {
- //Unknown network error...
- LOGGER.debug(GlobalVal.DEFAULT, "[!GDC!] Network error; poll failure Status code: ${response.code} " +
- "| ${response.message} | $responseBody")
-
- Mono.error(GoogleAuthCancelException())
- }
- }
- }.then()
- }
-}
diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/discal/LoginResponse.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/discal/LoginResponse.kt
index b8a78f104..7e48957eb 100644
--- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/discal/LoginResponse.kt
+++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/discal/LoginResponse.kt
@@ -1,9 +1,5 @@
package org.dreamexposure.discal.cam.json.discal
-import kotlinx.serialization.Serializable
-
-@Serializable
data class LoginResponse(
-
val link: String
)
diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/discal/TokenRequest.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/discal/TokenRequest.kt
index c5737d2f9..bcb54c8e2 100644
--- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/discal/TokenRequest.kt
+++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/discal/TokenRequest.kt
@@ -1,10 +1,6 @@
package org.dreamexposure.discal.cam.json.discal
-import kotlinx.serialization.Serializable
-
-@Serializable
data class TokenRequest(
val state: String,
-
val code: String,
)
diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/discal/TokenResponse.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/discal/TokenResponse.kt
index 8c365802a..dcb2c643b 100644
--- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/discal/TokenResponse.kt
+++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/discal/TokenResponse.kt
@@ -1,16 +1,10 @@
package org.dreamexposure.discal.cam.json.discal
-import kotlinx.serialization.Serializable
import org.dreamexposure.discal.cam.json.discord.SimpleUserData
-import org.dreamexposure.discal.core.serializers.InstantAsStringSerializer
import java.time.Instant
-@Serializable
data class TokenResponse(
val token: String,
-
- @Serializable(with = InstantAsStringSerializer::class)
val expires: Instant,
-
val user: SimpleUserData,
)
diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/discord/AccessTokenResponse.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/discord/AccessTokenResponse.kt
index 538182d9a..38d570d05 100644
--- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/discord/AccessTokenResponse.kt
+++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/discord/AccessTokenResponse.kt
@@ -1,20 +1,18 @@
package org.dreamexposure.discal.cam.json.discord
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
+import com.fasterxml.jackson.annotation.JsonProperty
-@Serializable
data class AccessTokenResponse(
- @SerialName("access_token")
+ @JsonProperty("access_token")
val accessToken: String,
- @SerialName("token_type")
+ @JsonProperty("token_type")
val type: String,
- @SerialName("expires_in")
+ @JsonProperty("expires_in")
val expiresIn: Long,
- @SerialName("refresh_token")
+ @JsonProperty("refresh_token")
val refreshToken: String,
val scope: String,
diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/discord/AuthorizationInfo.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/discord/AuthorizationInfo.kt
index 778614607..d2e320e45 100644
--- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/discord/AuthorizationInfo.kt
+++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/discord/AuthorizationInfo.kt
@@ -1,15 +1,9 @@
package org.dreamexposure.discal.cam.json.discord
-import kotlinx.serialization.Serializable
-import org.dreamexposure.discal.core.serializers.InstantAsStringSerializer
import java.time.Instant
-@Serializable
data class AuthorizationInfo(
val scopes: List,
-
- @Serializable(with = InstantAsStringSerializer::class)
val expires: Instant,
-
val user: SimpleUserData?,
)
diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/discord/SimpleUserData.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/discord/SimpleUserData.kt
index 69b9bf1d5..c5d3d20d2 100644
--- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/discord/SimpleUserData.kt
+++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/discord/SimpleUserData.kt
@@ -1,17 +1,10 @@
package org.dreamexposure.discal.cam.json.discord
import discord4j.common.util.Snowflake
-import kotlinx.serialization.Serializable
-import org.dreamexposure.discal.core.serializers.SnowflakeAsStringSerializer
-@Serializable
data class SimpleUserData(
- @Serializable(with = SnowflakeAsStringSerializer::class)
val id: Snowflake,
-
val username: String,
-
val discriminator: String,
-
val avatar: String?,
)
diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/google/ErrorData.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/google/ErrorData.kt
index bef773257..73a3f2c4a 100644
--- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/google/ErrorData.kt
+++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/google/ErrorData.kt
@@ -1,8 +1,6 @@
package org.dreamexposure.discal.cam.json.google
-import kotlinx.serialization.Serializable
-@Serializable
data class ErrorData(
- val error: String
+ val error: String
)
diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/google/RefreshData.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/google/RefreshData.kt
index 95c75f386..870da416b 100644
--- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/google/RefreshData.kt
+++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/google/RefreshData.kt
@@ -1,13 +1,11 @@
package org.dreamexposure.discal.cam.json.google
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
+import com.fasterxml.jackson.annotation.JsonProperty
-@Serializable
data class RefreshData(
- @SerialName("access_token")
+ @JsonProperty("access_token")
val accessToken: String,
- @SerialName("expires_in")
+ @JsonProperty("expires_in")
val expiresIn: Int
)
diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/managers/CalendarAuthManager.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/managers/CalendarAuthManager.kt
new file mode 100644
index 000000000..b337711ae
--- /dev/null
+++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/managers/CalendarAuthManager.kt
@@ -0,0 +1,29 @@
+package org.dreamexposure.discal.cam.managers
+
+import discord4j.common.util.Snowflake
+import org.dreamexposure.discal.cam.google.GoogleAuth
+import org.dreamexposure.discal.core.business.CalendarService
+import org.dreamexposure.discal.core.enums.calendar.CalendarHost
+import org.dreamexposure.discal.core.`object`.network.discal.CredentialData
+import org.springframework.stereotype.Component
+
+@Component
+class CalendarAuthManager(
+ private val calendarService: CalendarService,
+ private val googleAuth: GoogleAuth,
+) {
+ suspend fun getCredentialData(host: CalendarHost, id: Int, guild: Snowflake?): CredentialData? {
+ return when (host) {
+ CalendarHost.GOOGLE -> {
+ if (guild == null) {
+ // Internal (owned by DisCal, should never go bad)
+ googleAuth.requestNewAccessToken(id)
+ } else {
+ // External (owned by user)
+ val calendar = calendarService.getCalendar(guild, id) ?: return null
+ googleAuth.requestNewAccessToken(calendar)
+ }
+ }
+ }
+ }
+}
diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/managers/DiscordOauthManager.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/managers/DiscordOauthManager.kt
new file mode 100644
index 000000000..e553c88b6
--- /dev/null
+++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/managers/DiscordOauthManager.kt
@@ -0,0 +1,63 @@
+package org.dreamexposure.discal.cam.managers
+
+import org.dreamexposure.discal.cam.business.OauthStateService
+import org.dreamexposure.discal.cam.discord.DiscordOauthHandler
+import org.dreamexposure.discal.cam.json.discal.TokenResponse
+import org.dreamexposure.discal.core.business.SessionService
+import org.dreamexposure.discal.core.config.Config
+import org.dreamexposure.discal.core.crypto.KeyGenerator
+import org.dreamexposure.discal.core.`object`.WebSession
+import org.dreamexposure.discal.core.utils.GlobalVal.discordApiUrl
+import org.springframework.http.HttpStatus
+import org.springframework.stereotype.Component
+import org.springframework.web.server.ResponseStatusException
+import java.net.URLEncoder
+import java.nio.charset.Charset.defaultCharset
+
+@Component
+class DiscordOauthManager(
+ private val sessionService: SessionService,
+ private val oauthStateService: OauthStateService,
+ private val discordOauthHandler: DiscordOauthHandler,
+) {
+ private final val redirectUrl = Config.URL_DISCORD_REDIRECT.getString()
+ private final val clientId = Config.DISCORD_APP_ID.getString()
+
+ private final val scopes = URLEncoder.encode("identify guilds", defaultCharset())
+ private final val encodedRedirectUrl = URLEncoder.encode(redirectUrl, defaultCharset())
+ private final val oauthLinkWithoutState = "$discordApiUrl/oauth2/authorize?client_id=$clientId&redirect_uri=$encodedRedirectUrl&response_type=code&scope=$scopes&prompt=none"
+
+ suspend fun getOauthLinkForLogin(): String {
+ val state = oauthStateService.generateState()
+
+ return "$oauthLinkWithoutState&state=$state"
+ }
+
+ suspend fun handleLogout(token: String) {
+ sessionService.deleteSession(token)
+ }
+
+ suspend fun handleCodeExchange(state: String, code: String): TokenResponse {
+ // Validate state
+ if (!oauthStateService.validateState(state)) {
+ // State invalid - 400
+ throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid state")
+ }
+
+ val dTokens = discordOauthHandler.doTokenExchange(code)
+ val authInfo = discordOauthHandler.getOauthInfo(dTokens.accessToken)
+ val apiToken = KeyGenerator.csRandomAlphaNumericString(64)
+ val session = WebSession(
+ apiToken,
+ authInfo.user!!.id,
+ accessToken = dTokens.accessToken,
+ refreshToken = dTokens.refreshToken
+ )
+
+ sessionService.removeAndInsertSession(session)
+
+ return TokenResponse(session.token, session.expiresAt, authInfo.user)
+
+ TODO()
+ }
+}
diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/service/HeartbeatService.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/service/HeartbeatService.kt
deleted file mode 100644
index 5ba687c06..000000000
--- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/service/HeartbeatService.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-package org.dreamexposure.discal.cam.service
-
-import kotlinx.serialization.encodeToString
-import okhttp3.Request
-import okhttp3.RequestBody.Companion.toRequestBody
-import okhttp3.Response
-import org.dreamexposure.discal.core.`object`.BotSettings
-import org.dreamexposure.discal.core.`object`.network.discal.InstanceData
-import org.dreamexposure.discal.core.`object`.rest.HeartbeatRequest
-import org.dreamexposure.discal.core.`object`.rest.HeartbeatType
-import org.dreamexposure.discal.core.logger.LOGGER
-import org.dreamexposure.discal.core.utils.GlobalVal
-import org.springframework.boot.ApplicationArguments
-import org.springframework.boot.ApplicationRunner
-import org.springframework.stereotype.Component
-import reactor.core.publisher.Flux
-import reactor.core.publisher.Mono
-import reactor.core.scheduler.Schedulers
-import java.time.Duration
-
-@Component
-class HeartbeatService : ApplicationRunner {
-
- private fun heartbeat(): Mono {
- return Mono.just(InstanceData())
- .map { data ->
- val requestBody = HeartbeatRequest(HeartbeatType.CAM, instanceData = data)
-
- val body = GlobalVal.JSON_FORMAT.encodeToString(requestBody).toRequestBody(GlobalVal.JSON)
-
- Request.Builder()
- .url("${BotSettings.API_URL.get()}/v2/status/heartbeat")
- .post(body)
- .header("Authorization", BotSettings.BOT_API_TOKEN.get())
- .header("Content-Type", "application/json")
- .build()
- }.flatMap {
- Mono.fromCallable(GlobalVal.HTTP_CLIENT.newCall(it)::execute)
- .subscribeOn(Schedulers.boundedElastic())
- .map(Response::close)
- }.doOnError {
- LOGGER.error(GlobalVal.DEFAULT, "[Heartbeat] Failed to heartbeat", it)
- }.onErrorResume { Mono.empty() }
- .then()
-
- }
-
- override fun run(args: ApplicationArguments?) {
- Flux.interval(Duration.ofMinutes(2))
- .flatMap { heartbeat() }
- .subscribe()
- }
-}
diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/service/SessionService.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/service/SessionService.kt
deleted file mode 100644
index c4e7f9fe0..000000000
--- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/service/SessionService.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package org.dreamexposure.discal.cam.service
-
-import org.dreamexposure.discal.core.database.DatabaseManager
-import org.dreamexposure.discal.core.logger.LOGGER
-import org.dreamexposure.discal.core.utils.GlobalVal
-import org.springframework.boot.ApplicationArguments
-import org.springframework.boot.ApplicationRunner
-import org.springframework.stereotype.Component
-import reactor.core.publisher.Flux
-import reactor.core.publisher.Mono
-import java.time.Duration
-
-@Component
-class SessionService : ApplicationRunner {
- override fun run(args: ApplicationArguments?) {
- Flux.interval(Duration.ofHours(24))
- .flatMap {
- DatabaseManager.deleteExpiredSessions()
- }.doOnError {
- LOGGER.error(GlobalVal.DEFAULT, "Session Service runner error", it)
- }.onErrorResume {
- Mono.empty()
- }.subscribe()
- }
-}
diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/service/StateService.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/service/StateService.kt
deleted file mode 100644
index 80c645dd9..000000000
--- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/service/StateService.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package org.dreamexposure.discal.cam.service
-
-import org.dreamexposure.discal.core.crypto.KeyGenerator
-import org.springframework.stereotype.Component
-import reactor.core.publisher.Flux
-import java.time.Duration
-import java.time.Instant
-import java.time.temporal.ChronoUnit
-import java.util.concurrent.ConcurrentHashMap
-
-@Component
-class StateService {
-
- private val states: MutableMap = ConcurrentHashMap()
-
- init {
- // occasionally remove expired/unused states
- Flux.interval(Duration.ofHours(1))
- .doOnNext {
- val toRemove = mutableListOf()
-
- states.forEach { (state, expires) ->
- if (expires.isBefore(Instant.now()))
- toRemove.add(state)
- }
-
- toRemove.forEach(states::remove)
- }.subscribe()
- }
-
- fun generateState(): String {
- val state = KeyGenerator.csRandomAlphaNumericString(64)
- states[state] = Instant.now().plus(5, ChronoUnit.MINUTES)
-
- return state
- }
-
- fun validateState(state: String): Boolean {
- val expiresAt = states[state]
- states.remove(state) // Remove state immediately to prevent replay attacks
-
- if (expiresAt != null && expiresAt.isAfter(Instant.now())) return true
-
- // If state is not valid or has expired, we return false
- return false
- }
-}
diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/spring/WebFluxConfig.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/spring/WebFluxConfig.kt
deleted file mode 100644
index edf646c8f..000000000
--- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/spring/WebFluxConfig.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-package org.dreamexposure.discal.cam.spring
-
-import io.r2dbc.spi.ConnectionFactories
-import io.r2dbc.spi.ConnectionFactory
-import io.r2dbc.spi.ConnectionFactoryOptions
-import org.dreamexposure.discal.core.`object`.BotSettings
-import org.dreamexposure.discal.core.utils.GlobalVal
-import org.springframework.context.annotation.Bean
-import org.springframework.context.annotation.Configuration
-import org.springframework.data.redis.connection.RedisPassword
-import org.springframework.data.redis.connection.RedisStandaloneConfiguration
-import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
-import org.springframework.http.codec.ServerCodecConfigurer
-import org.springframework.http.codec.json.KotlinSerializationJsonDecoder
-import org.springframework.http.codec.json.KotlinSerializationJsonEncoder
-import org.springframework.web.reactive.config.EnableWebFlux
-import org.springframework.web.reactive.config.WebFluxConfigurer
-
-@Configuration
-@EnableWebFlux
-class WebFluxConfig: WebFluxConfigurer {
-
- @Bean(name = ["redisDatasource"])
- fun redisConnectionFactory(): LettuceConnectionFactory {
- val rsc = RedisStandaloneConfiguration()
- rsc.hostName = BotSettings.REDIS_HOSTNAME.get()
- rsc.port = BotSettings.REDIS_PORT.get().toInt()
- if (BotSettings.REDIS_USE_PASSWORD.get().equals("true", true))
- rsc.password = RedisPassword.of(BotSettings.REDIS_PASSWORD.get())
-
- return LettuceConnectionFactory(rsc)
- }
-
- @Bean(name = ["mysqlDatasource"])
- fun mysqlConnectionFactory(): ConnectionFactory {
- return ConnectionFactories.get(ConnectionFactoryOptions.builder()
- .option(ConnectionFactoryOptions.DRIVER, "pool")
- .option(ConnectionFactoryOptions.PROTOCOL, "mysql")
- .option(ConnectionFactoryOptions.HOST, BotSettings.SQL_HOST.get())
- .option(ConnectionFactoryOptions.PORT, BotSettings.SQL_PORT.get().toInt())
- .option(ConnectionFactoryOptions.USER, BotSettings.SQL_USER.get())
- .option(ConnectionFactoryOptions.PASSWORD, BotSettings.SQL_PASS.get())
- .option(ConnectionFactoryOptions.DATABASE, BotSettings.SQL_DB.get())
- .build())
- }
-
- override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
- val codecs = configurer.defaultCodecs()
- codecs.kotlinSerializationJsonDecoder(KotlinSerializationJsonDecoder(GlobalVal.JSON_FORMAT))
- codecs.kotlinSerializationJsonEncoder(KotlinSerializationJsonEncoder(GlobalVal.JSON_FORMAT))
- }
-}
diff --git a/cam/src/main/resources/application.properties b/cam/src/main/resources/application.properties
deleted file mode 100644
index e9a9464de..000000000
--- a/cam/src/main/resources/application.properties
+++ /dev/null
@@ -1,4 +0,0 @@
-spring.application.name=DisCal CAM
-server.port=8080
-spring.session.store-type=redis
-discal.security.enabled=true
diff --git a/client/build.gradle.kts b/client/build.gradle.kts
index 9d231f410..1644f5025 100644
--- a/client/build.gradle.kts
+++ b/client/build.gradle.kts
@@ -1,19 +1,18 @@
plugins {
+ // Kotlin
+ id("org.jetbrains.kotlin.plugin.allopen")
+
+ // Spring
kotlin("plugin.spring")
id("org.springframework.boot")
- id("org.jetbrains.kotlin.plugin.allopen")
+ id("io.spring.dependency-management")
+
+ // Tooling
id("com.google.cloud.tools.jib")
}
-val springVersion: String by properties
-val springSessionVersion: String by properties
-val springR2Version: String by properties
-
dependencies {
api(project(":core"))
-
- implementation("org.springframework.session:spring-session-data-redis:$springSessionVersion")
- implementation("org.springframework:spring-r2dbc:$springR2Version")
}
kotlin {
@@ -25,14 +24,13 @@ kotlin {
}
jib {
- var imageVersion = version.toString()
- if (imageVersion.contains("SNAPSHOT")) imageVersion = "latest"
+ to {
+ image = "rg.nl-ams.scw.cloud/dreamexposure/discal-client"
+ tags = mutableSetOf("latest", version.toString())
+ }
- to.image = "rg.nl-ams.scw.cloud/dreamexposure/discal-client:$imageVersion"
val baseImage: String by properties
from.image = baseImage
-
- container.creationTime = "USE_CURRENT_TIMESTAMP"
}
tasks {
diff --git a/client/src/main/java/org/dreamexposure/discal/client/listeners/discord/MessageCreateListener.java b/client/src/main/java/org/dreamexposure/discal/client/listeners/discord/MessageCreateListener.java
deleted file mode 100644
index dfec9d424..000000000
--- a/client/src/main/java/org/dreamexposure/discal/client/listeners/discord/MessageCreateListener.java
+++ /dev/null
@@ -1,81 +0,0 @@
-package org.dreamexposure.discal.client.listeners.discord;
-
-import discord4j.core.event.domain.message.MessageCreateEvent;
-import discord4j.core.object.entity.Message;
-import org.dreamexposure.discal.client.module.command.CommandExecutor;
-import org.dreamexposure.discal.core.database.DatabaseManager;
-import org.dreamexposure.discal.core.object.BotSettings;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import reactor.core.publisher.Mono;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-import static org.dreamexposure.discal.core.utils.GlobalVal.getDEFAULT;
-
-@SuppressWarnings("OptionalGetWithoutIsPresent")
-public class MessageCreateListener {
- private final static Logger LOGGER = LoggerFactory.getLogger(BotMentionListener.class);
-
- public static Mono handle(final MessageCreateEvent event) {
- return Mono.just(event.getMessage())
- .filter(message -> !message.getContent().isEmpty())
- .filterWhen(message ->
- Mono.justOrEmpty(event.getMember()
- .map(member -> !member.isBot()))
- )
- .map(Message::getContent)
- .flatMap(content ->
- DatabaseManager.INSTANCE.getSettings(event.getGuildId().get()).flatMap(settings -> {
- if (content.startsWith(settings.getPrefix())) {
- final String[] cmdAndArgs = content.trim().split("\\s+");
- if (cmdAndArgs.length > 1) {
- //command with args
- final String cmd = cmdAndArgs[0].replace(settings.getPrefix(), "");
- final List args = Arrays.asList(cmdAndArgs).subList(1, cmdAndArgs.length);
-
- //issue command
- return CommandExecutor.issueCommand(cmd, args, event, settings);
- } else if (cmdAndArgs.length == 1) {
- //Only command, no args
- final String cmd = cmdAndArgs[0].replace(settings.getPrefix(), "");
-
- //Issue command
- return CommandExecutor.issueCommand(cmd, new ArrayList<>(), event, settings);
- }
- return Mono.empty();
- } else if (!event.getMessage().mentionsEveryone()
- && !content.contains("@here")
- && (content.startsWith("<@" + BotSettings.ID.get() + ">")
- || content.startsWith("<@!" + BotSettings.ID.get() + ">"))) {
- final String[] cmdAndArgs = content.split("\\s+");
- if (cmdAndArgs.length > 2) {
- //DisCal mentioned with command and args
- final String cmd = cmdAndArgs[1];
- final List args = Arrays.asList(cmdAndArgs).subList(2, cmdAndArgs.length);
-
- //issue command
- return CommandExecutor.issueCommand(cmd, args, event, settings);
- } else if (cmdAndArgs.length == 2) {
- //DisCal mentioned with command and no args
- final String cmd = cmdAndArgs[1];
- //Issue command
- return CommandExecutor.issueCommand(cmd, new ArrayList<>(), event, settings);
- } else if (cmdAndArgs.length == 1) {
- //DisCal mentioned, nothing else
- return CommandExecutor.issueCommand("DisCal", new ArrayList<>(), event, settings);
- }
-
- return Mono.empty();
- } else {
- //Bot not mentioned, and this is not a command, ignore this
- return Mono.empty();
- }
- }))
- .doOnError(e -> LOGGER.error(getDEFAULT(), "Error handle message event | " + event.getMessage().getContent(), e))
- .onErrorResume(e -> Mono.empty())
- .then();
- }
-}
diff --git a/client/src/main/java/org/dreamexposure/discal/client/listeners/discord/package-info.java b/client/src/main/java/org/dreamexposure/discal/client/listeners/discord/package-info.java
deleted file mode 100644
index 0d1bfb7ac..000000000
--- a/client/src/main/java/org/dreamexposure/discal/client/listeners/discord/package-info.java
+++ /dev/null
@@ -1 +0,0 @@
-package org.dreamexposure.discal.client.listeners.discord;
\ No newline at end of file
diff --git a/client/src/main/java/org/dreamexposure/discal/client/message/Messages.java b/client/src/main/java/org/dreamexposure/discal/client/message/Messages.java
deleted file mode 100644
index d22d4411b..000000000
--- a/client/src/main/java/org/dreamexposure/discal/client/message/Messages.java
+++ /dev/null
@@ -1,151 +0,0 @@
-package org.dreamexposure.discal.client.message;
-
-import discord4j.core.event.domain.message.MessageCreateEvent;
-import discord4j.core.object.entity.Message;
-import discord4j.core.object.entity.User;
-import discord4j.core.object.entity.channel.GuildMessageChannel;
-import discord4j.core.spec.EmbedCreateSpec;
-import discord4j.rest.http.client.ClientException;
-import org.dreamexposure.discal.core.file.ReadFile;
-import org.dreamexposure.discal.core.object.GuildSettings;
-import org.dreamexposure.discal.core.utils.GlobalVal;
-import org.json.JSONObject;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import reactor.core.publisher.Mono;
-
-/**
- * @author NovaFox161
- * Date Created: 9/8/2018
- * For Project: DisCal-Discord-Bot
- * Author Website: https://www.novamaday.com
- * Company Website: https://www.dreamexposure.org
- * Contact: nova@dreamexposure.org
- */
-public class Messages {
- private static JSONObject langs;
- private final static Logger LOGGER = LoggerFactory.getLogger(Messages.class);
-
- //Lang handling
- @Deprecated
- public static Mono reloadLangs() {
- return ReadFile.readAllLangFiles()
- .doOnNext(l -> langs = l)
- .thenReturn(true)
- .doOnError(e -> LOGGER.error("[LANGS] Failed to reload lang files", e))
- .onErrorReturn(false);
- }
-
- @Deprecated
- public static String getMessage(String key, GuildSettings settings) {
- JSONObject messages;
-
- if (langs.has(settings.getLang()))
- messages = langs.getJSONObject(settings.getLang());
- else
- messages = langs.getJSONObject("ENGLISH");
-
- if (messages.has(key))
- return messages.getString(key).replace("%lb%", GlobalVal.getLineBreak());
- else
- return "***FAILSAFE MESSAGE*** MESSAGE NOT FOUND!! Message requested: " + key;
- }
-
- @Deprecated
- public static String getMessage(String key, String var, String replace, GuildSettings settings) {
- JSONObject messages;
-
- if (langs.has(settings.getLang()))
- messages = langs.getJSONObject(settings.getLang());
- else
- messages = langs.getJSONObject("ENGLISH");
-
- if (messages.has(key))
- return messages.getString(key).replace(var, replace).replace("%lb%", GlobalVal.getLineBreak());
- else
- return "***FAILSAFE MESSAGE*** MESSAGE NOT FOUND!! Message requested: " + key;
- }
-
- //Message sending
- public static Mono sendMessage(String message, MessageCreateEvent event) {
- return event.getMessage().getChannel()
- .flatMap(c -> c.createMessage(message))
- .onErrorResume(ClientException.class, e -> Mono.empty());
- }
-
- public static Mono sendMessage(EmbedCreateSpec embed, MessageCreateEvent event) {
- return event.getMessage().getChannel()
- .flatMap(c -> c.createMessage(embed))
- .onErrorResume(ClientException.class, e -> Mono.empty());
- }
-
- public static Mono sendMessage(String message, EmbedCreateSpec embed, MessageCreateEvent event) {
- return event.getMessage().getChannel()
- .flatMap(c -> c.createMessage(message)
- .withEmbeds(embed)
- )
- .onErrorResume(ClientException.class, e -> Mono.empty());
- }
-
- public static Mono sendMessage(String message, GuildMessageChannel channel) {
- return channel.createMessage(message)
- .onErrorResume(ClientException.class, e -> Mono.empty());
- }
-
- public static Mono sendMessage(EmbedCreateSpec embed, GuildMessageChannel channel) {
- return channel.createMessage(embed)
- .onErrorResume(ClientException.class, e -> Mono.empty());
- }
-
- public static Mono sendMessage(String message, EmbedCreateSpec embed, GuildMessageChannel channel) {
- return channel.createMessage(message).withEmbeds(embed)
- .onErrorResume(ClientException.class, e -> Mono.empty());
- }
-
- //Direct message sending
- public static Mono sendDirectMessage(String message, User user) {
- return user.getPrivateChannel()
- .flatMap(c -> c.createMessage(message))
- .onErrorResume(ClientException.class, e -> Mono.empty());
- }
-
- public static Mono sendDirectMessage(EmbedCreateSpec embed, User user) {
- return user.getPrivateChannel()
- .flatMap(c -> c.createMessage(embed))
- .onErrorResume(ClientException.class, e -> Mono.empty());
- }
-
- public static Mono sendDirectMessage(String message, EmbedCreateSpec embed, User user) {
- return user.getPrivateChannel()
- .flatMap(c -> c.createMessage(message).withEmbeds(embed))
- .onErrorResume(ClientException.class, e -> Mono.empty());
- }
-
- //Message editing
- public static Mono editMessage(String message, Message original) {
- return original.edit().withContentOrNull(message);
- }
-
- public static Mono editMessage(String message, EmbedCreateSpec embed, Message original) {
- return original.edit().withContentOrNull(message).withEmbeds(embed);
- }
-
- public static Mono editMessage(String message, MessageCreateEvent event) {
- return event.getMessage().edit().withContentOrNull(message);
- }
-
- public static Mono editMessage(String message, EmbedCreateSpec embed, MessageCreateEvent event) {
- return event.getMessage().edit().withContentOrNull(message).withEmbeds(embed);
- }
-
- //Message deleting
- public static Mono deleteMessage(Message message) {
- return Mono.justOrEmpty(message)
- .flatMap(Message::delete)
- .onErrorResume(e -> Mono.empty());
- }
-
- public static Mono deleteMessage(MessageCreateEvent event) {
- return deleteMessage(event.getMessage());
- }
-}
diff --git a/client/src/main/java/org/dreamexposure/discal/client/message/package-info.java b/client/src/main/java/org/dreamexposure/discal/client/message/package-info.java
deleted file mode 100644
index 08eb48d52..000000000
--- a/client/src/main/java/org/dreamexposure/discal/client/message/package-info.java
+++ /dev/null
@@ -1 +0,0 @@
-package org.dreamexposure.discal.client.message;
\ No newline at end of file
diff --git a/client/src/main/java/org/dreamexposure/discal/client/module/command/AddCalendarCommand.java b/client/src/main/java/org/dreamexposure/discal/client/module/command/AddCalendarCommand.java
deleted file mode 100644
index 8281238bd..000000000
--- a/client/src/main/java/org/dreamexposure/discal/client/module/command/AddCalendarCommand.java
+++ /dev/null
@@ -1,133 +0,0 @@
-package org.dreamexposure.discal.client.module.command;
-
-import discord4j.core.event.domain.message.MessageCreateEvent;
-import discord4j.core.object.entity.Message;
-import org.dreamexposure.discal.client.message.Messages;
-import org.dreamexposure.discal.client.network.google.GoogleExternalAuthHandler;
-import org.dreamexposure.discal.core.database.DatabaseManager;
-import org.dreamexposure.discal.core.enums.calendar.CalendarHost;
-import org.dreamexposure.discal.core.object.GuildSettings;
-import org.dreamexposure.discal.core.object.calendar.CalendarData;
-import org.dreamexposure.discal.core.object.command.CommandInfo;
-import org.dreamexposure.discal.core.utils.PermissionChecker;
-import org.dreamexposure.discal.core.wrapper.google.CalendarWrapper;
-import reactor.core.publisher.Flux;
-import reactor.core.publisher.Mono;
-
-import java.util.ArrayList;
-
-/**
- * Created by Nova Fox on 6/29/2017.
- * Website: www.cloudcraftgaming.com
- * For Project: DisCal
- */
-public class AddCalendarCommand implements Command {
- /**
- * Gets the command this Object is responsible for.
- *
- * @return The command this Object is responsible for.
- */
- @Override
- public String getCommand() {
- return "addCalendar";
- }
-
- /**
- * Gets the short aliases of the command this object is responsible for.
- *
- * This will return an empty ArrayList if none are present
- *
- * @return The aliases of the command.
- */
- @Override
- public ArrayList getAliases() {
- final ArrayList aliases = new ArrayList<>();
- aliases.add("addcal");
-
- return aliases;
- }
-
- /**
- * Gets the info on the command (not sub command) to be used in help menus.
- *
- * @return The command info.
- */
- @Override
- public CommandInfo getCommandInfo() {
- return new CommandInfo(
- "addCalendar",
- "Starts the process of adding an external calendar",
- "!addCalendar (calendar ID)"
- );
- }
-
- /**
- * Issues the command this Object is responsible for.
- *
- * @param args The command arguments.
- * @param event The event received.
- * @return {@code true} if successful, else {@code false}.
- */
- @Override
- public Mono issueCommand(final String[] args, final MessageCreateEvent event, final GuildSettings settings) {
- if (!(settings.getDevGuild() || settings.getPatronGuild())) {
- return Messages.sendMessage(Messages.getMessage("Notification.Patron", settings), event).then();
- }
-
- return PermissionChecker.hasManageServerRole(event).flatMap(hasManageRole -> {
- if (!hasManageRole) {
- return Messages.sendMessage(Messages.getMessage("Notification.Perm.MANAGE_SERVER", settings), event);
- }
-
- if (args.length == 0) {
- return DatabaseManager.INSTANCE.getMainCalendar(settings.getGuildID())
- .hasElement()
- .flatMap(hasCal -> {
- if (hasCal) {
- return Messages.sendMessage(
- Messages.getMessage("Creator.Calendar.HasCalendar", settings), event);
- } else {
- return GoogleExternalAuthHandler.INSTANCE.requestCode(event, settings)
- .then(Messages.sendMessage(
- Messages.getMessage("AddCalendar.Start", settings), event));
- }
- });
- } else if (args.length == 1) {
- return DatabaseManager.INSTANCE.getMainCalendar(settings.getGuildID())
- .flatMap(data -> {
- if (!"primary".equalsIgnoreCase(data.getCalendarAddress())) {
- return Messages.sendMessage(Messages.getMessage("Creator.Calendar.HasCalendar", settings), event);
- } else if ("N/a".equalsIgnoreCase(data.getEncryptedAccessToken())
- && "N/a".equalsIgnoreCase(data.getEncryptedRefreshToken())) {
- return Messages.sendMessage(Messages.getMessage("AddCalendar.Select.NotAuth", settings), event);
- } else {
- return CalendarWrapper.INSTANCE.getUsersExternalCalendars(data)
- .flatMapMany(Flux::fromIterable)
- .any(c -> !c.isDeleted() && c.getId().equals(args[0]))
- .flatMap(valid -> {
- if (valid) {
- final CalendarData newData = new CalendarData(
- data.getGuildId(), 1, CalendarHost.GOOGLE, args[0], args[0], true, 0,
- data.getPrivateKey(), data.getEncryptedAccessToken(),
- data.getEncryptedRefreshToken(), data.getExpiresAt());
-
- //combine db calls and message send to be executed together async
- final Mono calInsert = DatabaseManager.INSTANCE.updateCalendar(newData);
- final Mono sendMsg = Messages.sendMessage(
- Messages.getMessage("AddCalendar.Select.Success", settings), event);
-
- return Mono.when(calInsert, sendMsg);
- } else {
- return Messages.sendMessage(Messages
- .getMessage("AddCalendar.Select.Failure.Invalid", settings), event);
- }
- });
- }
- });
- } else {
- //Invalid argument count...
- return Messages.sendMessage(Messages.getMessage("AddCalendar.Specify", settings), event);
- }
- }).then();
- }
-}
diff --git a/client/src/main/java/org/dreamexposure/discal/client/module/command/Command.java b/client/src/main/java/org/dreamexposure/discal/client/module/command/Command.java
deleted file mode 100644
index e095c1472..000000000
--- a/client/src/main/java/org/dreamexposure/discal/client/module/command/Command.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package org.dreamexposure.discal.client.module.command;
-
-import org.dreamexposure.discal.core.object.GuildSettings;
-import org.dreamexposure.discal.core.object.command.CommandInfo;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import discord4j.core.event.domain.message.MessageCreateEvent;
-import reactor.core.publisher.Mono;
-
-/**
- * @author NovaFox161
- * Date Created: 9/10/2018
- * For Project: DisCal-Discord-Bot
- * Author Website: https://www.novamaday.com
- * Company Website: https://www.dreamexposure.org
- * Contact: nova@dreamexposure.org
- */
-public interface Command {
- default String getCommand() {
- return "COMMAND_NAME";
- }
-
- default List getAliases() {
- final List aliases = new ArrayList<>();
-
- aliases.add("ALIAS");
-
- return aliases;
- }
-
- default CommandInfo getCommandInfo() {
- final CommandInfo info = new CommandInfo(
- "COMMAND_NAME",
- "COMMAND_DESCRIPTION",
- "!command (sub2)"
- );
-
- info.getSubCommands().put("a", "b");
-
- return info;
- }
-
- Mono issueCommand(String[] args, MessageCreateEvent event, GuildSettings settings);
-}
\ No newline at end of file
diff --git a/client/src/main/java/org/dreamexposure/discal/client/module/command/CommandExecutor.java b/client/src/main/java/org/dreamexposure/discal/client/module/command/CommandExecutor.java
deleted file mode 100644
index b470a8d4f..000000000
--- a/client/src/main/java/org/dreamexposure/discal/client/module/command/CommandExecutor.java
+++ /dev/null
@@ -1,158 +0,0 @@
-package org.dreamexposure.discal.client.module.command;
-
-import discord4j.common.util.Snowflake;
-import discord4j.core.event.domain.message.MessageCreateEvent;
-import discord4j.core.spec.EmbedCreateSpec;
-import discord4j.rest.RestClient;
-import org.dreamexposure.discal.client.message.Messages;
-import org.dreamexposure.discal.core.object.BotSettings;
-import org.dreamexposure.discal.core.object.GuildSettings;
-import org.dreamexposure.discal.core.object.command.CommandInfo;
-import org.dreamexposure.discal.core.utils.GeneralUtils;
-import org.dreamexposure.discal.core.utils.GlobalVal;
-import reactor.core.publisher.Flux;
-import reactor.core.publisher.Mono;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Created by Nova Fox on 1/3/2017.
- * Website: www.cloudcraftgaming.com
- * For Project: DisCal
- */
-public class CommandExecutor {
- private static final List commands = new ArrayList<>();
-
- private static final List movedCommands = List.of(
- "help", "discal", "time", "linkcal", "calendarlink", "callink", "linkcallador", "events",
- "rsvp", "dev", "calendar", "cal", "callador", "event", "e",
- "announcement", "announcements", "announce", "alert", "alerts", "a", "ann"
- );
-
- private CommandExecutor() {
- }
-
- //Functional
-
- /**
- * Registers a command that can be executed.
- *
- * @param _command The command to register.
- */
- public static void registerCommand(final Command _command) {
- commands.add(_command);
- }
-
- public static Mono issueCommand(final String cmd, final List argsOr, final MessageCreateEvent event, final GuildSettings settings) {
- final String[] args;
- if (!argsOr.isEmpty()) {
- final String toParse = GeneralUtils.getContent(argsOr, 0);
- args = GeneralUtils.overkillParser(toParse).split(" ");
- } else {
- args = new String[0];
- }
-
- if (movedCommands.contains(cmd)) {
- return new WarningCommand().issueCommand(args, event, settings);
- } else
- return getCommand(cmd).flatMap(c -> c.issueCommand(args, event, settings));
- }
-
- /**
- * Gets an ArrayList of all valid commands.
- *
- * @return An ArrayList of all valid commands.
- */
- public static ArrayList getAllCommands() {
- final ArrayList cmds = new ArrayList<>();
- for (final Command c : commands) {
- if (!cmds.contains(c.getCommand()))
- cmds.add(c.getCommand());
- }
- return cmds;
- }
-
- public static List getCommands() {
- return commands;
- }
-
- private static Mono getCommand(final String cmdNameOrAlias) {
- return Flux.fromIterable(commands)
- .filter(c ->
- c.getCommand().equalsIgnoreCase(cmdNameOrAlias)
- || c.getAliases().contains(cmdNameOrAlias.toLowerCase()))
- .next();
- }
-}
-
-class WarningCommand implements Command {
-
- @Override
- public String getCommand() {
- return "\u200Bdiscal-command-moved-warning";
- }
-
- @Override
- public List getAliases() {
- return new ArrayList<>();
- }
-
- @Override
- public CommandInfo getCommandInfo() {
- return new CommandInfo("", "", "");
- }
-
- @Override
- public Mono issueCommand(String[] args, MessageCreateEvent event, GuildSettings settings) {
- //Check if slash commands are enabled in this server.
- RestClient restClient = event.getClient().getRestClient();
- //noinspection OptionalGetWithoutIsPresent (always present)
- Snowflake guildId = event.getGuildId().get();
-
- return restClient.getApplicationId().flatMapMany(appId ->
- restClient.getApplicationService().getGuildApplicationCommands(appId, guildId.asLong())
- ).collectList()
- .thenReturn(true)
- .onErrorReturn(false)
- .map(hasAppCommands -> {
- var builder = EmbedCreateSpec.builder()
- .author("DisCal", BotSettings.BASE_URL.get(), GlobalVal.getIconUrl())
- .color(GlobalVal.getDiscalColor())
- .title("DisCal Bot");
-
- if (hasAppCommands) {
- builder.description(
- String.format(enabledMessage,
- BotSettings.BASE_URL.get() + "/commands",
- BotSettings.SUPPORT_INVITE.get()
- )
- );
- } else {
- builder.description(
- String.format(disabledMessage,
- BotSettings.INVITE_URL.get(),
- BotSettings.BASE_URL.get() + "/commands",
- BotSettings.SUPPORT_INVITE.get()
- )
- );
- }
-
- return builder.build();
- }).flatMap(embed -> Messages.sendMessage(embed, event)).then();
- }
-
- private final String enabledMessage = """
- This command has been converted to a [Slash Command](https://discord.com/blog/slash-commands-are-here).
-
-
- For more information on commands, check out our [Commands Page](%s).
- For support, [join our server](%s).""";
-
- private final String disabledMessage = """
- This command has been converted to a [Slash Command](https://discord.com/blog/slash-commands-are-here), but they aren't enabled in this guild! A guild admin can [click here to enable them](%s).
-
-
- For more information on commands, check out our [Commands Page](%s).
- For support, [join our server](%s).""";
-}
diff --git a/client/src/main/java/org/dreamexposure/discal/client/module/command/package-info.java b/client/src/main/java/org/dreamexposure/discal/client/module/command/package-info.java
deleted file mode 100644
index 75b5ea161..000000000
--- a/client/src/main/java/org/dreamexposure/discal/client/module/command/package-info.java
+++ /dev/null
@@ -1 +0,0 @@
-package org.dreamexposure.discal.client.module.command;
\ No newline at end of file
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/DisCalClient.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/DisCalClient.kt
index 63522671b..8c02ab7a2 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/DisCalClient.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/DisCalClient.kt
@@ -1,157 +1,29 @@
package org.dreamexposure.discal.client
-import discord4j.common.store.Store
-import discord4j.common.store.legacy.LegacyStoreLayout
-import discord4j.core.DiscordClientBuilder
-import discord4j.core.GatewayDiscordClient
-import discord4j.core.`object`.presence.ClientActivity
-import discord4j.core.`object`.presence.ClientPresence
-import discord4j.core.event.domain.interaction.ChatInputInteractionEvent
-import discord4j.core.event.domain.lifecycle.ReadyEvent
-import discord4j.core.event.domain.message.MessageCreateEvent
-import discord4j.core.event.domain.role.RoleDeleteEvent
-import discord4j.core.shard.MemberRequestFilter
-import discord4j.core.shard.ShardingStrategy
-import discord4j.discordjson.json.GuildData
-import discord4j.discordjson.json.MessageData
-import discord4j.gateway.intent.Intent
-import discord4j.gateway.intent.IntentSet
-import discord4j.store.api.mapping.MappingStoreService
-import discord4j.store.api.service.StoreService
-import discord4j.store.jdk.JdkStoreService
-import discord4j.store.redis.RedisStoreService
-import io.lettuce.core.RedisClient
-import io.lettuce.core.RedisURI
import org.dreamexposure.discal.Application
-import org.dreamexposure.discal.client.listeners.discord.*
-import org.dreamexposure.discal.client.message.Messages
-import org.dreamexposure.discal.client.module.command.AddCalendarCommand
-import org.dreamexposure.discal.client.module.command.CommandExecutor
-import org.dreamexposure.discal.core.`object`.BotSettings
-import org.dreamexposure.discal.core.database.DatabaseManager
+import org.dreamexposure.discal.core.config.Config
import org.dreamexposure.discal.core.logger.LOGGER
import org.dreamexposure.discal.core.utils.GlobalVal.DEFAULT
-import org.dreamexposure.discal.core.utils.GlobalVal.STATUS
import org.springframework.boot.builder.SpringApplicationBuilder
import org.springframework.stereotype.Component
-import reactor.core.publisher.Mono
-import java.io.FileReader
-import java.util.*
-import javax.annotation.PreDestroy
import kotlin.system.exitProcess
@Component
class DisCalClient {
- companion object {
- @JvmStatic
- @Deprecated("Try to use client that is provided by d4j entities until using DI")
- var client: GatewayDiscordClient? = null
- private set
+ companion object {
@JvmStatic
fun main(args: Array) {
- //Get settings
- val p = Properties()
- p.load(FileReader("settings.properties"))
- BotSettings.init(p)
-
- //Load lang files
- Messages.reloadLangs().subscribe()
-
- //Register commands
- CommandExecutor.registerCommand(AddCalendarCommand())
+ Config.init()
//Start Spring
- val spring = try {
- SpringApplicationBuilder(Application::class.java)
- .profiles(BotSettings.PROFILE.get())
- .build()
- .run(*args)
+ try {
+ SpringApplicationBuilder(Application::class.java).run(*args)
} catch (e: Exception) {
- LOGGER.error(DEFAULT, "'Spring error' by PANIC! at the Bot", e)
+ LOGGER.error(DEFAULT, "Spring error!", e)
exitProcess(4)
}
-
- //Login
- DiscordClientBuilder.create(BotSettings.TOKEN.get())
- .build().gateway()
- .setEnabledIntents(getIntents())
- .setSharding(getStrategy())
- .setStore(Store.fromLayout(LegacyStoreLayout.of(getStores())))
- .setInitialPresence { ClientPresence.doNotDisturb(ClientActivity.playing("Booting Up!")) }
- .setMemberRequestFilter(MemberRequestFilter.none()) // TODO: remove after no longer needing members intent
- .withGateway { client ->
- DisCalClient.client = client
-
- //Register listeners
- val onReady = client
- .on(ReadyEvent::class.java, ReadyEventListener::handle)
- .then()
-
- val onRoleDelete = client
- .on(RoleDeleteEvent::class.java, RoleDeleteListener::handle)
- .then()
-
- val onCommand = client
- .on(MessageCreateEvent::class.java, MessageCreateListener::handle)
- .then()
-
- val onMention = client
- .on(MessageCreateEvent::class.java, BotMentionListener::handle)
- .then()
-
- val slashCommandListener = SlashCommandListener(spring)
- val onSlashCommand = client
- .on(ChatInputInteractionEvent::class.java, slashCommandListener::handle)
- .then()
-
- Mono.`when`(onReady, onRoleDelete, onCommand, onSlashCommand)
- }.block()
}
}
-
-
- @PreDestroy
- fun onShutdown() {
- LOGGER.info(STATUS, "Shutting down shard")
-
- DatabaseManager.disconnectFromMySQL()
-
- client?.logout()?.subscribe()
- }
-}
-
-private fun getStrategy(): ShardingStrategy {
- return ShardingStrategy.builder()
- .count(Application.getShardCount())
- .indices(Application.getShardIndex().toInt())
- .build()
-}
-
-private fun getStores(): StoreService {
- return if (BotSettings.USE_REDIS_STORES.get().equals("true", ignoreCase = true)) {
- val uri = RedisURI.Builder
- .redis(BotSettings.REDIS_HOSTNAME.get(), BotSettings.REDIS_PORT.get().toInt())
- .withPassword(BotSettings.REDIS_PASSWORD.get().toCharArray())
- .build()
-
- val rss = RedisStoreService.Builder()
- .redisClient(RedisClient.create(uri))
- .build()
-
- MappingStoreService.create()
- .setMappings(rss, GuildData::class.java, MessageData::class.java)
- .setFallback(JdkStoreService())
- } else JdkStoreService()
}
-private fun getIntents(): IntentSet {
- return IntentSet.of(
- Intent.GUILDS,
- Intent.GUILD_MEMBERS,
- Intent.GUILD_MESSAGES,
- Intent.GUILD_MESSAGE_REACTIONS,
- Intent.DIRECT_MESSAGES,
- Intent.DIRECT_MESSAGE_REACTIONS
- )
-}
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/SlashCommand.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/SlashCommand.kt
index afea62e40..c04553a1a 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/SlashCommand.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/SlashCommand.kt
@@ -1,7 +1,9 @@
package org.dreamexposure.discal.client.commands
-import discord4j.core.`object`.entity.Message
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent
+import discord4j.core.`object`.entity.Message
+import kotlinx.coroutines.reactor.awaitSingle
+import kotlinx.coroutines.reactor.mono
import org.dreamexposure.discal.core.`object`.GuildSettings
import org.dreamexposure.discal.core.utils.MessageSourceLoader
import reactor.core.publisher.Mono
@@ -11,7 +13,14 @@ interface SlashCommand {
val ephemeral: Boolean
- fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Mono
+ @Deprecated("Use new handleSuspend for K-coroutines")
+ fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Mono {
+ return mono { suspendHandle(event, settings) }
+ }
+
+ suspend fun suspendHandle(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
+ return handle(event, settings).awaitSingle()
+ }
fun getMessage(key: String, settings: GuildSettings, vararg args: String): String {
val src = MessageSourceLoader.getSourceByPath("command/$name/$name")
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/DevCommand.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/dev/DevCommand.kt
similarity index 97%
rename from client/src/main/kotlin/org/dreamexposure/discal/client/commands/DevCommand.kt
rename to client/src/main/kotlin/org/dreamexposure/discal/client/commands/dev/DevCommand.kt
index 88b5e4507..5b86be54f 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/DevCommand.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/dev/DevCommand.kt
@@ -1,9 +1,10 @@
-package org.dreamexposure.discal.client.commands
+package org.dreamexposure.discal.client.commands.dev
import discord4j.core.`object`.command.ApplicationCommandInteractionOption
import discord4j.core.`object`.command.ApplicationCommandInteractionOptionValue
import discord4j.core.`object`.entity.Message
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent
+import org.dreamexposure.discal.client.commands.SlashCommand
import org.dreamexposure.discal.core.`object`.GuildSettings
import org.dreamexposure.discal.core.`object`.web.UserAPIAccount
import org.dreamexposure.discal.core.crypto.KeyGenerator.csRandomAlphaNumericString
@@ -19,6 +20,7 @@ class DevCommand : SlashCommand {
override val name = "dev"
override val ephemeral = true
+ @Deprecated("Use new handleSuspend for K-coroutines")
override fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Mono {
if (!GlobalVal.devUserIds.contains(event.interaction.user.id)) {
return event.followupEphemeral(getMessage("error.notDeveloper", settings))
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/AnnouncementCommand.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/AnnouncementCommand.kt
similarity index 99%
rename from client/src/main/kotlin/org/dreamexposure/discal/client/commands/AnnouncementCommand.kt
rename to client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/AnnouncementCommand.kt
index 9b6620386..375e0329c 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/AnnouncementCommand.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/AnnouncementCommand.kt
@@ -1,17 +1,15 @@
-package org.dreamexposure.discal.client.commands
+package org.dreamexposure.discal.client.commands.global
+import discord4j.core.event.domain.interaction.ChatInputInteractionEvent
import discord4j.core.`object`.command.ApplicationCommandInteractionOption
import discord4j.core.`object`.command.ApplicationCommandInteractionOptionValue
import discord4j.core.`object`.entity.Member
import discord4j.core.`object`.entity.Message
import discord4j.core.`object`.entity.channel.MessageChannel
-import discord4j.core.event.domain.interaction.ChatInputInteractionEvent
import discord4j.core.spec.InteractionFollowupCreateSpec
import discord4j.rest.util.AllowedMentions
+import org.dreamexposure.discal.client.commands.SlashCommand
import org.dreamexposure.discal.client.message.embed.AnnouncementEmbed
-import org.dreamexposure.discal.core.`object`.GuildSettings
-import org.dreamexposure.discal.core.`object`.Wizard
-import org.dreamexposure.discal.core.`object`.announcement.Announcement
import org.dreamexposure.discal.core.crypto.KeyGenerator
import org.dreamexposure.discal.core.database.DatabaseManager
import org.dreamexposure.discal.core.enums.announcement.AnnouncementType
@@ -21,6 +19,9 @@ import org.dreamexposure.discal.core.extensions.discord4j.followupEphemeral
import org.dreamexposure.discal.core.extensions.discord4j.getCalendar
import org.dreamexposure.discal.core.extensions.discord4j.hasControlRole
import org.dreamexposure.discal.core.extensions.messageContentSafe
+import org.dreamexposure.discal.core.`object`.GuildSettings
+import org.dreamexposure.discal.core.`object`.Wizard
+import org.dreamexposure.discal.core.`object`.announcement.Announcement
import org.dreamexposure.discal.core.utils.getCommonMsg
import org.springframework.stereotype.Component
import reactor.core.publisher.Flux
@@ -31,6 +32,7 @@ class AnnouncementCommand(val wizard: Wizard) : SlashCommand {
override val name = "announcement"
override val ephemeral = true
+ @Deprecated("Use new handleSuspend for K-coroutines")
override fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Mono {
return when (event.options[0].name) {
"create" -> create(event, settings)
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/CalendarCommand.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/CalendarCommand.kt
similarity index 98%
rename from client/src/main/kotlin/org/dreamexposure/discal/client/commands/CalendarCommand.kt
rename to client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/CalendarCommand.kt
index ebe6bf1c9..54c458649 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/CalendarCommand.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/CalendarCommand.kt
@@ -1,4 +1,4 @@
-package org.dreamexposure.discal.client.commands
+package org.dreamexposure.discal.client.commands.global
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent
import discord4j.core.`object`.command.ApplicationCommandInteractionOption
@@ -6,6 +6,7 @@ import discord4j.core.`object`.command.ApplicationCommandInteractionOptionValue
import discord4j.core.`object`.entity.Guild
import discord4j.core.`object`.entity.Member
import discord4j.core.`object`.entity.Message
+import org.dreamexposure.discal.client.commands.SlashCommand
import org.dreamexposure.discal.client.message.embed.CalendarEmbed
import org.dreamexposure.discal.client.service.StaticMessageService
import org.dreamexposure.discal.core.entities.response.UpdateCalendarResponse
@@ -27,6 +28,7 @@ class CalendarCommand(val wizard: Wizard, val staticMessageSrv: Sta
override val name = "calendar"
override val ephemeral = true
+ @Deprecated("Use new handleSuspend for K-coroutines")
override fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Mono {
return when (event.options[0].name) {
"create" -> create(event, settings)
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/DiscalCommand.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/DiscalCommand.kt
similarity index 80%
rename from client/src/main/kotlin/org/dreamexposure/discal/client/commands/DiscalCommand.kt
rename to client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/DiscalCommand.kt
index 7e8277793..e9875cf9f 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/DiscalCommand.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/DiscalCommand.kt
@@ -1,7 +1,8 @@
-package org.dreamexposure.discal.client.commands
+package org.dreamexposure.discal.client.commands.global
import discord4j.core.`object`.entity.Message
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent
+import org.dreamexposure.discal.client.commands.SlashCommand
import org.dreamexposure.discal.client.message.embed.DiscalEmbed
import org.dreamexposure.discal.core.`object`.GuildSettings
import org.dreamexposure.discal.core.extensions.discord4j.followup
@@ -13,6 +14,7 @@ class DiscalCommand : SlashCommand {
override val name = "discal"
override val ephemeral = false
+ @Deprecated("Use new handleSuspend for K-coroutines")
override fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Mono {
return event.interaction.guild
.flatMap(DiscalEmbed::info)
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/DisplayCalendarCommand.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/DisplayCalendarCommand.kt
similarity index 97%
rename from client/src/main/kotlin/org/dreamexposure/discal/client/commands/DisplayCalendarCommand.kt
rename to client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/DisplayCalendarCommand.kt
index 744b6c306..9def46dc2 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/DisplayCalendarCommand.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/DisplayCalendarCommand.kt
@@ -1,4 +1,4 @@
-package org.dreamexposure.discal.client.commands
+package org.dreamexposure.discal.client.commands.global
import discord4j.common.util.Snowflake
import discord4j.core.`object`.command.ApplicationCommandInteractionOption
@@ -8,6 +8,7 @@ import discord4j.core.`object`.entity.Message
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent
import discord4j.core.spec.MessageCreateSpec
import discord4j.rest.http.client.ClientException
+import org.dreamexposure.discal.client.commands.SlashCommand
import org.dreamexposure.discal.client.message.embed.CalendarEmbed
import org.dreamexposure.discal.core.`object`.GuildSettings
import org.dreamexposure.discal.core.`object`.StaticMessage
@@ -28,6 +29,7 @@ class DisplayCalendarCommand : SlashCommand {
override val name = "displaycal"
override val ephemeral = true
+ @Deprecated("Use new handleSuspend for K-coroutines")
override fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Mono {
return when (event.options[0].name) {
"new" -> new(event, settings)
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/EventCommand.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/EventCommand.kt
similarity index 95%
rename from client/src/main/kotlin/org/dreamexposure/discal/client/commands/EventCommand.kt
rename to client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/EventCommand.kt
index f9004565b..86358b931 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/EventCommand.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/EventCommand.kt
@@ -1,4 +1,4 @@
-package org.dreamexposure.discal.client.commands
+package org.dreamexposure.discal.client.commands.global
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent
import discord4j.core.`object`.command.ApplicationCommandInteractionOption
@@ -6,6 +6,7 @@ import discord4j.core.`object`.command.ApplicationCommandInteractionOptionValue
import discord4j.core.`object`.entity.Member
import discord4j.core.`object`.entity.Message
import discord4j.core.spec.MessageCreateSpec
+import org.dreamexposure.discal.client.commands.SlashCommand
import org.dreamexposure.discal.client.message.embed.EventEmbed
import org.dreamexposure.discal.client.service.StaticMessageService
import org.dreamexposure.discal.core.entities.Event
@@ -33,6 +34,7 @@ class EventCommand(val wizard: Wizard, val staticMessageSrv: StaticMes
override val name = "event"
override val ephemeral = true
+ @Deprecated("Use new handleSuspend for K-coroutines")
override fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Mono {
return when (event.options[0].name) {
"create" -> create(event, settings)
@@ -171,6 +173,10 @@ class EventCommand(val wizard: Wizard, val staticMessageSrv: StaticMes
.map(Long::toInt)
.map { it.coerceAtLeast(0).coerceAtMost(59) }
.orElse(0)
+ val keepDuration = event.options[0].getOption("keep-duration")
+ .flatMap(ApplicationCommandInteractionOption::getValue)
+ .map(ApplicationCommandInteractionOptionValue::asBoolean)
+ .orElse(settings.eventKeepDuration)
return Mono.justOrEmpty(event.interaction.member).filterWhen(Member::hasControlRole).flatMap {
val pre = wizard.get(settings.guildID)
@@ -196,8 +202,13 @@ class EventCommand(val wizard: Wizard, val staticMessageSrv: StaticMes
}
} else {
// Event end already set, make sure everything is in order
- if (pre.end!!.isAfter(start)) {
+ val originalDuration = if (pre.start != null) Duration.between(pre.start, pre.end) else null
+ val shouldChangeDuration = keepDuration && originalDuration != null
+
+ if (pre.end!!.isAfter(start) || shouldChangeDuration) {
pre.start = start
+ if (shouldChangeDuration) pre.end = start.plus(originalDuration)
+
if (pre.start!!.isAfter(Instant.now())) {
event.interaction.guild
.map { EventEmbed.pre(it, settings, pre) }
@@ -251,6 +262,10 @@ class EventCommand(val wizard: Wizard, val staticMessageSrv: StaticMes
.map(Long::toInt)
.map { it.coerceAtLeast(0).coerceAtMost(59) }
.orElse(0)
+ val keepDuration = event.options[0].getOption("keep-duration")
+ .flatMap(ApplicationCommandInteractionOption::getValue)
+ .map(ApplicationCommandInteractionOptionValue::asBoolean)
+ .orElse(settings.eventKeepDuration)
return Mono.justOrEmpty(event.interaction.member).filterWhen(Member::hasControlRole).flatMap {
val pre = wizard.get(settings.guildID)
@@ -276,8 +291,13 @@ class EventCommand(val wizard: Wizard, val staticMessageSrv: StaticMes
}
} else {
// Event start already set, make sure everything is in order
- if (pre.start!!.isBefore(end)) {
+ val originalDuration = if (pre.end != null) Duration.between(pre.start, pre.end) else null
+ val shouldChangeDuration = keepDuration && originalDuration != null
+
+ if (pre.start!!.isBefore(end) || shouldChangeDuration) {
pre.end = end
+ if (shouldChangeDuration) pre.start = end.minus(originalDuration)
+
if (pre.end!!.isAfter(Instant.now())) {
event.interaction.guild
.map { EventEmbed.pre(it, settings, pre) }
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/EventsCommand.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/EventsCommand.kt
similarity index 98%
rename from client/src/main/kotlin/org/dreamexposure/discal/client/commands/EventsCommand.kt
rename to client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/EventsCommand.kt
index f8658f3bf..df7e2316c 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/EventsCommand.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/EventsCommand.kt
@@ -1,9 +1,10 @@
-package org.dreamexposure.discal.client.commands
+package org.dreamexposure.discal.client.commands.global
import discord4j.core.`object`.command.ApplicationCommandInteractionOption
import discord4j.core.`object`.command.ApplicationCommandInteractionOptionValue
import discord4j.core.`object`.entity.Message
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent
+import org.dreamexposure.discal.client.commands.SlashCommand
import org.dreamexposure.discal.client.message.embed.EventEmbed
import org.dreamexposure.discal.core.`object`.GuildSettings
import org.dreamexposure.discal.core.extensions.discord4j.followup
@@ -24,6 +25,7 @@ class EventsCommand : SlashCommand {
override val name = "events"
override val ephemeral = false
+ @Deprecated("Use new handleSuspend for K-coroutines")
override fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Mono {
return when (event.options[0].name) {
"upcoming" -> upcomingEventsSubcommand(event, settings)
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/HelpCommand.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/HelpCommand.kt
similarity index 65%
rename from client/src/main/kotlin/org/dreamexposure/discal/client/commands/HelpCommand.kt
rename to client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/HelpCommand.kt
index a8427f1f4..7d1f398d1 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/HelpCommand.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/HelpCommand.kt
@@ -1,10 +1,11 @@
-package org.dreamexposure.discal.client.commands
+package org.dreamexposure.discal.client.commands.global
-import discord4j.core.`object`.entity.Message
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent
-import org.dreamexposure.discal.core.`object`.BotSettings
-import org.dreamexposure.discal.core.`object`.GuildSettings
+import discord4j.core.`object`.entity.Message
+import org.dreamexposure.discal.client.commands.SlashCommand
+import org.dreamexposure.discal.core.config.Config
import org.dreamexposure.discal.core.extensions.discord4j.followupEphemeral
+import org.dreamexposure.discal.core.`object`.GuildSettings
import org.springframework.stereotype.Component
import reactor.core.publisher.Mono
@@ -13,9 +14,10 @@ class HelpCommand : SlashCommand {
override val name = "help"
override val ephemeral = true
+ @Deprecated("Use new handleSuspend for K-coroutines")
override fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Mono {
return event.followupEphemeral(
- getMessage("error.workInProgress", settings, "${BotSettings.BASE_URL.get()}/commands")
+ getMessage("error.workInProgress", settings, "${Config.URL_BASE.getString()}/commands")
)
}
}
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/LinkCalendarCommand.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/LinkCalendarCommand.kt
similarity index 90%
rename from client/src/main/kotlin/org/dreamexposure/discal/client/commands/LinkCalendarCommand.kt
rename to client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/LinkCalendarCommand.kt
index 72d9c126d..108df5ea1 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/LinkCalendarCommand.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/LinkCalendarCommand.kt
@@ -1,9 +1,10 @@
-package org.dreamexposure.discal.client.commands
+package org.dreamexposure.discal.client.commands.global
import discord4j.core.`object`.command.ApplicationCommandInteractionOption
import discord4j.core.`object`.command.ApplicationCommandInteractionOptionValue
import discord4j.core.`object`.entity.Message
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent
+import org.dreamexposure.discal.client.commands.SlashCommand
import org.dreamexposure.discal.client.message.embed.CalendarEmbed
import org.dreamexposure.discal.core.`object`.GuildSettings
import org.dreamexposure.discal.core.extensions.discord4j.followup
@@ -16,6 +17,7 @@ class LinkCalendarCommand : SlashCommand {
override val name = "linkcal"
override val ephemeral = false
+ @Deprecated("Use new handleSuspend for K-coroutines")
override fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Mono {
val showOverview = event.getOption("overview")
.flatMap(ApplicationCommandInteractionOption::getValue)
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/RsvpCommand.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/RsvpCommand.kt
similarity index 99%
rename from client/src/main/kotlin/org/dreamexposure/discal/client/commands/RsvpCommand.kt
rename to client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/RsvpCommand.kt
index 0bc307556..baa8be3b4 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/RsvpCommand.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/RsvpCommand.kt
@@ -1,10 +1,11 @@
-package org.dreamexposure.discal.client.commands
+package org.dreamexposure.discal.client.commands.global
import discord4j.core.`object`.command.ApplicationCommandInteractionOption
import discord4j.core.`object`.command.ApplicationCommandInteractionOptionValue
import discord4j.core.`object`.entity.Member
import discord4j.core.`object`.entity.Message
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent
+import org.dreamexposure.discal.client.commands.SlashCommand
import org.dreamexposure.discal.client.message.embed.RsvpEmbed
import org.dreamexposure.discal.core.`object`.GuildSettings
import org.dreamexposure.discal.core.extensions.discord4j.followupEphemeral
@@ -21,6 +22,7 @@ class RsvpCommand : SlashCommand {
override val name = "rsvp"
override val ephemeral = true
+ @Deprecated("Use new handleSuspend for K-coroutines")
override fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Mono {
return when (event.options[0].name) {
"ontime" -> onTime(event, settings)
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/SettingsCommand.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/SettingsCommand.kt
similarity index 86%
rename from client/src/main/kotlin/org/dreamexposure/discal/client/commands/SettingsCommand.kt
rename to client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/SettingsCommand.kt
index 27f2e3f70..a3ddc9582 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/SettingsCommand.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/SettingsCommand.kt
@@ -1,17 +1,18 @@
-package org.dreamexposure.discal.client.commands
+package org.dreamexposure.discal.client.commands.global
+import discord4j.core.event.domain.interaction.ChatInputInteractionEvent
import discord4j.core.`object`.command.ApplicationCommandInteractionOption
import discord4j.core.`object`.command.ApplicationCommandInteractionOptionValue
import discord4j.core.`object`.entity.Message
-import discord4j.core.event.domain.interaction.ChatInputInteractionEvent
+import org.dreamexposure.discal.client.commands.SlashCommand
import org.dreamexposure.discal.client.message.embed.SettingsEmbed
-import org.dreamexposure.discal.core.`object`.GuildSettings
import org.dreamexposure.discal.core.database.DatabaseManager
import org.dreamexposure.discal.core.enums.announcement.AnnouncementStyle
import org.dreamexposure.discal.core.enums.time.TimeFormat
import org.dreamexposure.discal.core.extensions.discord4j.followup
import org.dreamexposure.discal.core.extensions.discord4j.followupEphemeral
import org.dreamexposure.discal.core.extensions.discord4j.hasElevatedPermissions
+import org.dreamexposure.discal.core.`object`.GuildSettings
import org.dreamexposure.discal.core.utils.getCommonMsg
import org.springframework.stereotype.Component
import reactor.core.publisher.Mono
@@ -21,6 +22,7 @@ class SettingsCommand : SlashCommand {
override val name = "settings"
override val ephemeral = true
+ @Deprecated("Use new handleSuspend for K-coroutines")
override fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Mono {
//Check if user has permission to use this
return event.interaction.member.get().hasElevatedPermissions().flatMap { hasPerm ->
@@ -31,6 +33,7 @@ class SettingsCommand : SlashCommand {
"announcement-style" -> announcementStyleSubcommand(event, settings)
"language" -> languageSubcommand(event, settings)
"time-format" -> timeFormatSubcommand(event, settings)
+ "keep-event-duration" -> eventKeepDurationSubcommand(event, settings)
"branding" -> brandingSubcommand(event, settings)
else -> Mono.empty() //Never can reach this, makes compiler happy.
}
@@ -98,6 +101,18 @@ class SettingsCommand : SlashCommand {
.flatMap { event.followupEphemeral(getMessage("format.success", settings, timeFormat.name)) }
}
+ private fun eventKeepDurationSubcommand(event: ChatInputInteractionEvent, settings: GuildSettings): Mono {
+ val keepDuration = event.options[0].getOption("value")
+ .flatMap(ApplicationCommandInteractionOption::getValue)
+ .map(ApplicationCommandInteractionOptionValue::asBoolean)
+ .get()
+
+ settings.eventKeepDuration = keepDuration
+
+ return DatabaseManager.updateSettings(settings)
+ .flatMap { event.followupEphemeral(getMessage("eventKeepDuration.success.$keepDuration", settings)) }
+ }
+
private fun brandingSubcommand(event: ChatInputInteractionEvent, settings: GuildSettings): Mono {
return if (settings.patronGuild) {
val useBranding = event.options[0].getOption("use")
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/TimeCommand.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/TimeCommand.kt
similarity index 78%
rename from client/src/main/kotlin/org/dreamexposure/discal/client/commands/TimeCommand.kt
rename to client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/TimeCommand.kt
index 526097661..51fec260d 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/TimeCommand.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/global/TimeCommand.kt
@@ -1,22 +1,23 @@
-package org.dreamexposure.discal.client.commands
+package org.dreamexposure.discal.client.commands.global
+import discord4j.core.event.domain.interaction.ChatInputInteractionEvent
import discord4j.core.`object`.command.ApplicationCommandInteractionOption
import discord4j.core.`object`.command.ApplicationCommandInteractionOptionValue
import discord4j.core.`object`.entity.Message
-import discord4j.core.event.domain.interaction.ChatInputInteractionEvent
+import kotlinx.coroutines.reactor.awaitSingle
+import org.dreamexposure.discal.client.commands.SlashCommand
import org.dreamexposure.discal.client.message.embed.CalendarEmbed
-import org.dreamexposure.discal.core.`object`.GuildSettings
import org.dreamexposure.discal.core.extensions.discord4j.followupEphemeral
+import org.dreamexposure.discal.core.`object`.GuildSettings
import org.dreamexposure.discal.core.utils.getCommonMsg
import org.springframework.stereotype.Component
-import reactor.core.publisher.Mono
@Component
class TimeCommand : SlashCommand {
override val name = "time"
override val ephemeral = true
- override fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Mono {
+ override suspend fun suspendHandle(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
val calendarNumber = event.getOption("calendar")
.flatMap(ApplicationCommandInteractionOption::getValue)
.map(ApplicationCommandInteractionOptionValue::asLong)
@@ -27,6 +28,6 @@ class TimeCommand : SlashCommand {
CalendarEmbed.time(guild, settings, calendarNumber).flatMap {
event.followupEphemeral(it)
}
- }.switchIfEmpty(event.followupEphemeral(getCommonMsg("error.notFound.calendar", settings)))
+ }.switchIfEmpty(event.followupEphemeral(getCommonMsg("error.notFound.calendar", settings))).awaitSingle()
}
}
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/AddCalCommand.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/premium/AddCalCommand.kt
similarity index 83%
rename from client/src/main/kotlin/org/dreamexposure/discal/client/commands/AddCalCommand.kt
rename to client/src/main/kotlin/org/dreamexposure/discal/client/commands/premium/AddCalCommand.kt
index 5d0c7f600..4cbd0903a 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/AddCalCommand.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/premium/AddCalCommand.kt
@@ -1,14 +1,15 @@
-package org.dreamexposure.discal.client.commands
+package org.dreamexposure.discal.client.commands.premium
+import discord4j.core.event.domain.interaction.ChatInputInteractionEvent
import discord4j.core.`object`.entity.Guild
import discord4j.core.`object`.entity.Member
import discord4j.core.`object`.entity.Message
-import discord4j.core.event.domain.interaction.ChatInputInteractionEvent
-import org.dreamexposure.discal.core.`object`.BotSettings
-import org.dreamexposure.discal.core.`object`.GuildSettings
+import org.dreamexposure.discal.client.commands.SlashCommand
+import org.dreamexposure.discal.core.config.Config
import org.dreamexposure.discal.core.extensions.discord4j.canAddCalendar
import org.dreamexposure.discal.core.extensions.discord4j.followupEphemeral
import org.dreamexposure.discal.core.extensions.discord4j.hasElevatedPermissions
+import org.dreamexposure.discal.core.`object`.GuildSettings
import org.dreamexposure.discal.core.utils.getCommonMsg
import org.springframework.stereotype.Component
import reactor.core.publisher.Mono
@@ -18,6 +19,7 @@ class AddCalCommand : SlashCommand {
override val name = "addcal"
override val ephemeral = true
+ @Deprecated("Use new handleSuspend for K-coroutines")
override fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Mono {
//TODO: Remove dev-only and switch to patron-only once this is completed
return if (settings.devGuild) {
@@ -33,6 +35,6 @@ class AddCalCommand : SlashCommand {
}
private fun getLink(settings: GuildSettings): String {
- return "${BotSettings.BASE_URL.get()}/dashboard/${settings.guildID.asString()}/calendar/new?type=1&step=0"
+ return "${Config.URL_BASE.getString()}/dashboard/${settings.guildID.asString()}/calendar/new?type=1&step=0"
}
}
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/conf/WebFluxConfig.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/conf/WebFluxConfig.kt
deleted file mode 100644
index f6d8e495c..000000000
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/conf/WebFluxConfig.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-package org.dreamexposure.discal.client.conf
-
-import io.r2dbc.spi.ConnectionFactories
-import io.r2dbc.spi.ConnectionFactory
-import io.r2dbc.spi.ConnectionFactoryOptions
-import org.dreamexposure.discal.core.`object`.BotSettings
-import org.dreamexposure.discal.core.utils.GlobalVal
-import org.springframework.boot.web.server.ConfigurableWebServerFactory
-import org.springframework.boot.web.server.ErrorPage
-import org.springframework.boot.web.server.WebServerFactoryCustomizer
-import org.springframework.context.annotation.Bean
-import org.springframework.context.annotation.Configuration
-import org.springframework.data.redis.connection.RedisPassword
-import org.springframework.data.redis.connection.RedisStandaloneConfiguration
-import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
-import org.springframework.http.HttpStatus
-import org.springframework.http.codec.ServerCodecConfigurer
-import org.springframework.http.codec.json.KotlinSerializationJsonDecoder
-import org.springframework.http.codec.json.KotlinSerializationJsonEncoder
-import org.springframework.web.reactive.config.CorsRegistry
-import org.springframework.web.reactive.config.EnableWebFlux
-import org.springframework.web.reactive.config.WebFluxConfigurer
-
-@Configuration
-@EnableWebFlux
-class WebFluxConfig : WebFluxConfigurer, WebServerFactoryCustomizer {
-
- override fun customize(factory: ConfigurableWebServerFactory?) {
- factory?.addErrorPages(ErrorPage(HttpStatus.NOT_FOUND, "/"))
- }
-
- override fun addCorsMappings(registry: CorsRegistry) {
- registry.addMapping("/api/**")
- .allowedOrigins("*")
- }
-
- @Bean(name = ["redisDatasource"])
- fun redisConnectionFactory(): LettuceConnectionFactory {
- val rsc = RedisStandaloneConfiguration()
- rsc.hostName = BotSettings.REDIS_HOSTNAME.get()
- rsc.port = BotSettings.REDIS_PORT.get().toInt()
- if (BotSettings.REDIS_USE_PASSWORD.get().equals("true", true))
- rsc.password = RedisPassword.of(BotSettings.REDIS_PASSWORD.get())
-
- return LettuceConnectionFactory(rsc)
- }
-
- @Bean(name = ["mysqlDatasource"])
- fun mysqlConnectionFactory(): ConnectionFactory {
- return ConnectionFactories.get(ConnectionFactoryOptions.builder()
- .option(ConnectionFactoryOptions.DRIVER, "pool")
- .option(ConnectionFactoryOptions.PROTOCOL, "mysql")
- .option(ConnectionFactoryOptions.HOST, BotSettings.SQL_HOST.get())
- .option(ConnectionFactoryOptions.PORT, BotSettings.SQL_PORT.get().toInt())
- .option(ConnectionFactoryOptions.USER, BotSettings.SQL_USER.get())
- .option(ConnectionFactoryOptions.PASSWORD, BotSettings.SQL_PASS.get())
- .option(ConnectionFactoryOptions.DATABASE, BotSettings.SQL_DB.get())
- .build())
- }
-
- override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
- val codecs = configurer.defaultCodecs()
- codecs.kotlinSerializationJsonDecoder(KotlinSerializationJsonDecoder(GlobalVal.JSON_FORMAT))
- codecs.kotlinSerializationJsonEncoder(KotlinSerializationJsonEncoder(GlobalVal.JSON_FORMAT))
- }
-}
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/conf/DisCalConfig.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/config/DisCalConfig.kt
similarity index 92%
rename from client/src/main/kotlin/org/dreamexposure/discal/client/conf/DisCalConfig.kt
rename to client/src/main/kotlin/org/dreamexposure/discal/client/config/DisCalConfig.kt
index ff26d457f..7d6ed92c8 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/conf/DisCalConfig.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/config/DisCalConfig.kt
@@ -1,4 +1,4 @@
-package org.dreamexposure.discal.client.conf
+package org.dreamexposure.discal.client.config
import org.dreamexposure.discal.core.`object`.Wizard
import org.dreamexposure.discal.core.`object`.announcement.Announcement
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/config/DiscordConfig.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/config/DiscordConfig.kt
new file mode 100644
index 000000000..99dd21854
--- /dev/null
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/config/DiscordConfig.kt
@@ -0,0 +1,114 @@
+package org.dreamexposure.discal.client.config
+
+import discord4j.common.store.Store
+import discord4j.common.store.legacy.LegacyStoreLayout
+import discord4j.core.DiscordClientBuilder
+import discord4j.core.GatewayDiscordClient
+import discord4j.core.event.domain.Event
+import discord4j.core.`object`.presence.ClientActivity
+import discord4j.core.`object`.presence.ClientPresence
+import discord4j.core.shard.MemberRequestFilter
+import discord4j.core.shard.ShardingStrategy
+import discord4j.discordjson.json.GuildData
+import discord4j.discordjson.json.MessageData
+import discord4j.gateway.intent.Intent
+import discord4j.gateway.intent.IntentSet
+import discord4j.rest.RestClient
+import discord4j.store.api.mapping.MappingStoreService
+import discord4j.store.api.service.StoreService
+import discord4j.store.jdk.JdkStoreService
+import discord4j.store.redis.RedisClusterStoreService
+import discord4j.store.redis.RedisStoreDefaults
+import discord4j.store.redis.RedisStoreService
+import io.lettuce.core.RedisClient
+import io.lettuce.core.RedisURI
+import io.lettuce.core.cluster.RedisClusterClient
+import kotlinx.coroutines.reactor.mono
+import org.dreamexposure.discal.Application
+import org.dreamexposure.discal.client.listeners.discord.EventListener
+import org.dreamexposure.discal.core.config.Config
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import reactor.kotlin.core.publisher.toFlux
+
+@Configuration
+class DiscordConfig {
+
+ @Bean
+ fun discordGatewayClient(
+ listeners: List>,
+ stores: StoreService
+ ): GatewayDiscordClient {
+ return DiscordClientBuilder.create(Config.SECRET_BOT_TOKEN.getString())
+ .build().gateway()
+ .setEnabledIntents(getIntents())
+ .setSharding(getStrategy())
+ .setStore(Store.fromLayout(LegacyStoreLayout.of(stores)))
+ .setInitialPresence { ClientPresence.doNotDisturb(ClientActivity.playing("Booting Up!")) }
+ .setMemberRequestFilter(MemberRequestFilter.none())
+ .withEventDispatcher { dispatcher ->
+ @Suppress("UNCHECKED_CAST")
+ (listeners as Iterable>).toFlux()
+ .flatMap {
+ dispatcher.on(it.genericType) { event -> mono { it.handle(event) } }
+ }
+ }
+ .login()
+ .block()!!
+ }
+
+ @Bean
+ fun discordRestClient(gatewayDiscordClient: GatewayDiscordClient): RestClient {
+ return gatewayDiscordClient.restClient
+ }
+
+ @Bean
+ fun discordStores(): StoreService {
+ val useRedis = Config.CACHE_USE_REDIS.getBoolean()
+ val redisHost = Config.REDIS_HOST.getString()
+ val redisPassword = Config.REDIS_PASSWORD.getString().toCharArray()
+ val redisPort = Config.REDIS_PORT.getInt()
+ val redisCluster = Config.CACHE_REDIS_IS_CLUSTER.getBoolean()
+ val prefix = Config.CACHE_PREFIX.getString()
+
+ return if (useRedis) {
+ val uriBuilder = RedisURI.Builder
+ .redis(redisHost, redisPort)
+ if (redisPassword.isNotEmpty()) uriBuilder.withPassword(redisPassword)
+
+ val rss = if (redisCluster) {
+ RedisClusterStoreService.Builder()
+ .redisClient(RedisClusterClient.create(uriBuilder.build()))
+ .keyPrefix("$prefix.${RedisStoreDefaults.DEFAULT_KEY_PREFIX}")
+ .build()
+ } else {
+ RedisStoreService.Builder()
+ .redisClient(RedisClient.create(uriBuilder.build()))
+ .keyPrefix("$prefix.${RedisStoreDefaults.DEFAULT_KEY_PREFIX}")
+ .build()
+ }
+
+
+ MappingStoreService.create()
+ .setMappings(rss, GuildData::class.java, MessageData::class.java)
+ .setFallback(JdkStoreService())
+ } else JdkStoreService()
+ }
+
+ private fun getStrategy(): ShardingStrategy {
+ return ShardingStrategy.builder()
+ .count(Application.getShardCount())
+ .indices(Application.getShardIndex())
+ .build()
+ }
+
+ private fun getIntents(): IntentSet {
+ return IntentSet.of(
+ Intent.GUILDS,
+ Intent.GUILD_MESSAGES,
+ Intent.GUILD_MESSAGE_REACTIONS,
+ Intent.DIRECT_MESSAGES,
+ Intent.DIRECT_MESSAGE_REACTIONS
+ )
+ }
+}
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/config/WebFluxConfig.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/config/WebFluxConfig.kt
new file mode 100644
index 000000000..ecb3cef3e
--- /dev/null
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/config/WebFluxConfig.kt
@@ -0,0 +1,34 @@
+package org.dreamexposure.discal.client.config
+
+import org.dreamexposure.discal.core.utils.GlobalVal
+import org.springframework.boot.web.server.ConfigurableWebServerFactory
+import org.springframework.boot.web.server.ErrorPage
+import org.springframework.boot.web.server.WebServerFactoryCustomizer
+import org.springframework.context.annotation.Configuration
+import org.springframework.http.HttpStatus
+import org.springframework.http.codec.ServerCodecConfigurer
+import org.springframework.http.codec.json.KotlinSerializationJsonDecoder
+import org.springframework.http.codec.json.KotlinSerializationJsonEncoder
+import org.springframework.web.reactive.config.CorsRegistry
+import org.springframework.web.reactive.config.EnableWebFlux
+import org.springframework.web.reactive.config.WebFluxConfigurer
+
+@Configuration
+@EnableWebFlux
+class WebFluxConfig : WebFluxConfigurer, WebServerFactoryCustomizer {
+
+ override fun customize(factory: ConfigurableWebServerFactory?) {
+ factory?.addErrorPages(ErrorPage(HttpStatus.NOT_FOUND, "/"))
+ }
+
+ override fun addCorsMappings(registry: CorsRegistry) {
+ registry.addMapping("/api/**")
+ .allowedOrigins("*")
+ }
+
+ override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
+ val codecs = configurer.defaultCodecs()
+ codecs.kotlinSerializationJsonDecoder(KotlinSerializationJsonDecoder(GlobalVal.JSON_FORMAT))
+ codecs.kotlinSerializationJsonEncoder(KotlinSerializationJsonEncoder(GlobalVal.JSON_FORMAT))
+ }
+}
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/listeners/discord/BotMentionListener.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/listeners/discord/BotMentionListener.kt
index 3b770c63f..8ea543ae7 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/listeners/discord/BotMentionListener.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/listeners/discord/BotMentionListener.kt
@@ -1,36 +1,31 @@
package org.dreamexposure.discal.client.listeners.discord
+import discord4j.core.event.domain.message.MessageCreateEvent
import discord4j.core.`object`.entity.Message
import discord4j.core.`object`.entity.User
-import discord4j.core.event.domain.message.MessageCreateEvent
+import kotlinx.coroutines.reactor.awaitSingle
+import kotlinx.coroutines.reactor.awaitSingleOrNull
import org.dreamexposure.discal.client.message.embed.DiscalEmbed
-import org.dreamexposure.discal.core.`object`.BotSettings.ID
-import reactor.core.publisher.Mono
-
-object BotMentionListener {
+import org.springframework.stereotype.Component
- fun handle(event: MessageCreateEvent): Mono {
- if (event.guildId.isPresent //in guild
- && !event.message.author.map(User::isBot).orElse(false) // not from a bot
- && containsMention(event.message) // mentions the bot
+@Component
+class BotMentionListener: EventListener {
+ override suspend fun handle(event: MessageCreateEvent) {
+ if (event.guildId.isPresent // in guild
+ && !event.message.author.map(User::isBot).orElse(false) // Not from a bot
+ && onlyMentionsBot(event.message)
) {
- return event.guild.flatMap(DiscalEmbed::info).flatMap { embed ->
- event.message.channel.flatMap { channel ->
- channel.createMessage(embed).then()
- }
- }
+ val embed = event.guild.flatMap(DiscalEmbed::info).awaitSingle()
+ val channel = event.message.channel.awaitSingle()
+
+ channel.createMessage(embed).awaitSingleOrNull()
}
- //Ignore everything else
- return Mono.empty()
}
- private fun containsMention(message: Message): Boolean {
- return message.userMentionIds.size == 1 && // only 1 user mentioned
- message.roleMentionIds.isEmpty() && // no roles mentioned
- message.userMentionIds.contains(message.client.selfId) && // only the bot is mentioned
- !message.mentionsEveryone() // no @everyone mentioned
+ private fun onlyMentionsBot(message: Message): Boolean {
+ return (message.userMentionIds.size == 1 && message.userMentionIds.contains(message.client.selfId)) // Only bot user mentioned
+ && message.roleMentionIds.isEmpty() // Does not mention any roles
+ && !message.mentionsEveryone() // Does not mention everyone
}
-
- private fun containsMention(msg: String) = msg == "<@${ID.get()}>" || msg == "<@!${ID.get()}>"
}
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/listeners/discord/EventListener.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/listeners/discord/EventListener.kt
new file mode 100644
index 000000000..9ce312446
--- /dev/null
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/listeners/discord/EventListener.kt
@@ -0,0 +1,12 @@
+package org.dreamexposure.discal.client.listeners.discord
+
+import discord4j.core.event.domain.Event
+import org.springframework.core.GenericTypeResolver
+
+interface EventListener {
+ @Suppress("UNCHECKED_CAST")
+ val genericType: Class
+ get() = GenericTypeResolver.resolveTypeArgument(javaClass, EventListener::class.java) as Class
+
+ suspend fun handle(event: T)
+}
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/listeners/discord/GuildCreateListener.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/listeners/discord/GuildCreateListener.kt
new file mode 100644
index 000000000..3cfbfc64a
--- /dev/null
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/listeners/discord/GuildCreateListener.kt
@@ -0,0 +1,59 @@
+package org.dreamexposure.discal.client.listeners.discord
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.readValue
+import discord4j.core.event.domain.guild.GuildCreateEvent
+import discord4j.discordjson.json.ApplicationCommandRequest
+import kotlinx.coroutines.reactor.awaitSingle
+import kotlinx.coroutines.reactor.awaitSingleOrNull
+import org.dreamexposure.discal.core.database.DatabaseManager
+import org.dreamexposure.discal.core.logger.LOGGER
+import org.dreamexposure.discal.core.utils.GlobalVal.DEFAULT
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver
+import org.springframework.stereotype.Component
+
+@Component
+class GuildCreateListener(objectMapper: ObjectMapper) : EventListener {
+ private val premiumCommands: List
+ private val devCommands: List
+
+ init {
+ val matcher = PathMatchingResourcePatternResolver()
+
+ // Get premium commands
+ val premiumCommands = mutableListOf()
+ for (res in matcher.getResources("commands/premium/*.json")) {
+ val request = objectMapper.readValue(res.inputStream)
+ premiumCommands.add(request)
+ }
+ this.premiumCommands = premiumCommands
+
+ // Get dev commands
+ val devCommands = mutableListOf()
+ for (res in matcher.getResources("commands/dev/*.json")) {
+ val request = objectMapper.readValue(res.inputStream)
+ premiumCommands.add(request)
+ }
+ this.devCommands = devCommands
+
+ }
+
+ override suspend fun handle(event: GuildCreateEvent) {
+ val settings = DatabaseManager.getSettings(event.guild.id).awaitSingle()
+ val appService = event.client.restClient.applicationService
+ val guildId = settings.guildID.asLong()
+ val appId = event.client.selfId.asLong()
+
+ val commands = mutableListOf()
+ if (settings.patronGuild) commands.addAll(premiumCommands)
+ if (settings.devGuild) commands.addAll(devCommands)
+
+ if (commands.isNotEmpty()) {
+ appService.bulkOverwriteGuildApplicationCommand(appId, guildId, commands)
+ .doOnNext { LOGGER.debug("Bulk guild overwrite read: ${it.name()} | $guildId") }
+ .doOnError { LOGGER.error(DEFAULT, "Bulk guild overwrite failed | $guildId", it) }
+ .then()
+ .awaitSingleOrNull()
+ }
+ }
+}
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/listeners/discord/ReadyEventListener.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/listeners/discord/ReadyEventListener.kt
index c3c49f8ca..18403e6b0 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/listeners/discord/ReadyEventListener.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/listeners/discord/ReadyEventListener.kt
@@ -2,22 +2,24 @@ package org.dreamexposure.discal.client.listeners.discord
import discord4j.core.event.domain.lifecycle.ReadyEvent
import discord4j.rest.util.Image
-import org.dreamexposure.discal.client.message.Messages
+import kotlinx.coroutines.reactor.awaitSingle
import org.dreamexposure.discal.core.logger.LOGGER
import org.dreamexposure.discal.core.utils.GlobalVal.DEFAULT
import org.dreamexposure.discal.core.utils.GlobalVal.STATUS
import org.dreamexposure.discal.core.utils.GlobalVal.iconUrl
-import reactor.core.publisher.Mono
+import org.springframework.stereotype.Component
-object ReadyEventListener {
- fun handle(event: ReadyEvent): Mono {
- return event.client.applicationInfo
- .doOnNext { iconUrl = it.getIconUrl(Image.Format.PNG).get() }
- .doOnNext { LOGGER.info(STATUS, "Ready event success!") }
- .then(Messages.reloadLangs())
- .onErrorResume {
- LOGGER.error(DEFAULT, "Failed to handle ready event")
- Mono.empty()
- }.then()
+@Component
+class ReadyEventListener : EventListener {
+ override suspend fun handle(event: ReadyEvent) {
+ try {
+ iconUrl = event.client.applicationInfo
+ .map { it.getIconUrl(Image.Format.PNG).orElse("") }
+ .awaitSingle()
+
+ LOGGER.info(STATUS, "Ready event success!")
+ } catch (e: Exception) {
+ LOGGER.error(DEFAULT, "Failed to handle ready event", e)
+ }
}
}
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/listeners/discord/RoleDeleteListener.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/listeners/discord/RoleDeleteListener.kt
index 85ab9853c..1c8f2510c 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/listeners/discord/RoleDeleteListener.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/listeners/discord/RoleDeleteListener.kt
@@ -2,20 +2,23 @@ package org.dreamexposure.discal.client.listeners.discord
import discord4j.common.util.Snowflake
import discord4j.core.event.domain.role.RoleDeleteEvent
+import kotlinx.coroutines.reactor.awaitSingleOrNull
import org.dreamexposure.discal.core.database.DatabaseManager
+import org.springframework.stereotype.Component
import reactor.core.publisher.Mono
-object RoleDeleteListener {
+@Component
+class RoleDeleteListener : EventListener {
- fun handle(event: RoleDeleteEvent): Mono {
+ override suspend fun handle(event: RoleDeleteEvent) {
val updateRsvps = DatabaseManager.removeRsvpRole(event.guildId, event.roleId)
val updateControlRole = DatabaseManager.getSettings(event.guildId)
- .filter { !"everyone".equals(it.controlRole, true) }
- .filter { event.roleId == Snowflake.of(it.controlRole) }
- .doOnNext { it.controlRole = "everyone" }
- .flatMap(DatabaseManager::updateSettings)
+ .filter { !"everyone".equals(it.controlRole, true) }
+ .filter { event.roleId == Snowflake.of(it.controlRole) }
+ .doOnNext { it.controlRole = "everyone" }
+ .flatMap(DatabaseManager::updateSettings)
- return Mono.`when`(updateRsvps, updateControlRole)
+ Mono.`when`(updateRsvps, updateControlRole).awaitSingleOrNull()
}
}
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/listeners/discord/SlashCommandListener.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/listeners/discord/SlashCommandListener.kt
index db6f9374e..82574f4bc 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/listeners/discord/SlashCommandListener.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/listeners/discord/SlashCommandListener.kt
@@ -1,38 +1,47 @@
package org.dreamexposure.discal.client.listeners.discord
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent
+import kotlinx.coroutines.reactor.awaitSingle
+import kotlinx.coroutines.reactor.awaitSingleOrNull
import org.dreamexposure.discal.client.commands.SlashCommand
import org.dreamexposure.discal.core.database.DatabaseManager
-import org.dreamexposure.discal.core.extensions.discord4j.followupEphemeral
import org.dreamexposure.discal.core.logger.LOGGER
-import org.dreamexposure.discal.core.utils.getCommonMsg
-import org.springframework.context.ApplicationContext
-import reactor.core.publisher.Flux
-import reactor.core.publisher.Mono
+import org.dreamexposure.discal.core.utils.GlobalVal.DEFAULT
+import org.springframework.stereotype.Component
-class SlashCommandListener(applicationContext: ApplicationContext) {
- private val cmds = applicationContext.getBeansOfType(SlashCommand::class.java).values
+@Component
+class SlashCommandListener(
+ private val commands: List
+) : EventListener {
- fun handle(event: ChatInputInteractionEvent): Mono {
+ override suspend fun handle(event: ChatInputInteractionEvent) {
if (!event.interaction.guildId.isPresent) {
- return event.reply("Commands not supported in DMs.")
+ event.reply("Commands not supported in DMs.").awaitSingleOrNull()
+ return
+ }
+
+ val command = commands.firstOrNull { it.name == event.commandName }
+
+ if (command != null) {
+ event.deferReply().withEphemeral(command.ephemeral).awaitSingleOrNull()
+
+ try {
+ val settings = DatabaseManager.getSettings(event.interaction.guildId.get()).awaitSingle()
+
+ command.suspendHandle(event, settings)
+ } catch (e: Exception) {
+ LOGGER.error(DEFAULT, "Error handling slash command | $event", e)
+
+ // Attempt to provide a message if there's an unhandled exception
+ event.createFollowup("An unknown error has occurred")
+ .withEphemeral(command.ephemeral)
+ .awaitSingleOrNull()
+ }
+ } else {
+ event.createFollowup("An unknown error has occurred. Please try again and/or contact DisCal support.")
+ .withEphemeral(true)
+ .awaitSingleOrNull()
}
- return Flux.fromIterable(cmds)
- .filter { it.name == event.commandName }
- .next()
- .flatMap { command ->
- val mono =
- if (command.ephemeral) event.deferReply().withEphemeral(true)
- else event.deferReply()
-
- mono.then(DatabaseManager.getSettings(event.interaction.guildId.get())).flatMap {
- command.handle(event, it).switchIfEmpty(event.followupEphemeral(getCommonMsg("error.unknown", it)))
- }
- }.doOnError {
- LOGGER.error("Unhandled slash command error", it)
- }.onErrorResume {
- event.followupEphemeral("An unknown error has occurred. Please try again and/or contact DisCal support.")
- }.then()
}
}
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/message/embed/DiscalEmbed.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/message/embed/DiscalEmbed.kt
index 5c9991615..5e5d9a3ed 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/message/embed/DiscalEmbed.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/message/embed/DiscalEmbed.kt
@@ -5,7 +5,7 @@ import discord4j.core.spec.EmbedCreateSpec
import org.dreamexposure.discal.Application
import org.dreamexposure.discal.GitProperty.DISCAL_VERSION
import org.dreamexposure.discal.GitProperty.DISCAL_VERSION_D4J
-import org.dreamexposure.discal.core.`object`.BotSettings
+import org.dreamexposure.discal.core.config.Config
import org.dreamexposure.discal.core.database.DatabaseManager
import org.dreamexposure.discal.core.extensions.discord4j.getSettings
import org.dreamexposure.discal.core.extensions.getHumanReadable
@@ -37,12 +37,12 @@ object DiscalEmbed : EmbedMaker {
.addField(getMessage("discal", "info.field.announcements", settings), "$ann", true)
.addField(getMessage("discal", "info.field.links", settings),
getMessage("discal",
- "info.field.links.value",
- settings,
- "${BotSettings.BASE_URL.get()}/commands",
- BotSettings.SUPPORT_INVITE.get(),
- BotSettings.INVITE_URL.get(),
- "https://www.patreon.com/Novafox"
+ "info.field.links.value",
+ settings,
+ "${Config.URL_BASE.getString()}/commands",
+ Config.URL_SUPPORT.getString(),
+ Config.URL_INVITE.getString(),
+ "https://www.patreon.com/Novafox"
),
false
).footer(getMessage("discal", "info.footer", settings), null)
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/message/embed/EmbedMaker.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/message/embed/EmbedMaker.kt
index 82635765a..7db29da57 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/message/embed/EmbedMaker.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/message/embed/EmbedMaker.kt
@@ -3,7 +3,7 @@ package org.dreamexposure.discal.client.message.embed
import discord4j.core.`object`.entity.Guild
import discord4j.core.spec.EmbedCreateSpec
import discord4j.rest.util.Image
-import org.dreamexposure.discal.core.`object`.BotSettings
+import org.dreamexposure.discal.core.config.Config
import org.dreamexposure.discal.core.`object`.GuildSettings
import org.dreamexposure.discal.core.utils.GlobalVal
import org.dreamexposure.discal.core.utils.getCommonMsg
@@ -14,9 +14,9 @@ interface EmbedMaker {
val builder = EmbedCreateSpec.builder()
if (settings.branded)
- builder.author(guild.name, BotSettings.BASE_URL.get(), guild.getIconUrl(Image.Format.PNG).orElse(GlobalVal.iconUrl))
+ builder.author(guild.name, Config.URL_BASE.getString(), guild.getIconUrl(Image.Format.PNG).orElse(GlobalVal.iconUrl))
else
- builder.author(getCommonMsg("bot.name", settings), BotSettings.BASE_URL.get(), GlobalVal.iconUrl)
+ builder.author(getCommonMsg("bot.name", settings), Config.URL_BASE.getString(), GlobalVal.iconUrl)
return builder
}
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/message/embed/SettingsEmbed.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/message/embed/SettingsEmbed.kt
index d38956bd0..70c94e109 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/message/embed/SettingsEmbed.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/message/embed/SettingsEmbed.kt
@@ -3,8 +3,8 @@ package org.dreamexposure.discal.client.message.embed
import discord4j.core.`object`.entity.Guild
import discord4j.core.`object`.entity.Role
import discord4j.core.spec.EmbedCreateSpec
-import org.dreamexposure.discal.core.`object`.GuildSettings
import org.dreamexposure.discal.core.extensions.discord4j.getControlRole
+import org.dreamexposure.discal.core.`object`.GuildSettings
import reactor.core.publisher.Mono
object SettingsEmbed : EmbedMaker {
@@ -16,6 +16,7 @@ object SettingsEmbed : EmbedMaker {
.addField(getMessage("settings", "view.field.role", settings), roleName, false)
.addField(getMessage("settings", "view.field.style", settings), settings.announcementStyle.name, true)
.addField(getMessage("settings", "view.field.format", settings), settings.timeFormat.name, true)
+ .addField(getMessage("settings", "view.field.eventKeepDuration", settings), "${settings.eventKeepDuration}", true)
.addField(getMessage("settings", "view.field.lang", settings), settings.getLocale().displayName, false)
.addField(getMessage("settings", "view.field.patron", settings), "${settings.patronGuild}", true)
.addField(getMessage("settings", "view.field.dev", settings), "${settings.devGuild}", true)
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/network/google/GoogleExternalAuthHandler.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/network/google/GoogleExternalAuthHandler.kt
deleted file mode 100644
index 80cfc3349..000000000
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/network/google/GoogleExternalAuthHandler.kt
+++ /dev/null
@@ -1,196 +0,0 @@
-package org.dreamexposure.discal.client.network.google
-
-import discord4j.core.event.domain.message.MessageCreateEvent
-import discord4j.core.spec.EmbedCreateSpec
-import org.dreamexposure.discal.client.message.Messages
-import org.dreamexposure.discal.core.`object`.BotSettings
-import org.dreamexposure.discal.core.`object`.GuildSettings
-import org.dreamexposure.discal.core.`object`.calendar.CalendarData
-import org.dreamexposure.discal.core.`object`.google.ExternalGoogleAuthPoll
-import org.dreamexposure.discal.core.crypto.AESEncryption
-import org.dreamexposure.discal.core.database.DatabaseManager
-import org.dreamexposure.discal.core.enums.calendar.CalendarHost
-import org.dreamexposure.discal.core.exceptions.google.GoogleAuthCancelException
-import org.dreamexposure.discal.core.logger.LOGGER
-import org.dreamexposure.discal.core.utils.GlobalVal
-import org.dreamexposure.discal.core.utils.GlobalVal.DEFAULT
-import org.dreamexposure.discal.core.utils.GlobalVal.discalColor
-import org.dreamexposure.discal.core.utils.GlobalVal.iconUrl
-import org.dreamexposure.discal.core.wrapper.google.CalendarWrapper
-import org.dreamexposure.discal.core.wrapper.google.GoogleAuthWrapper
-import org.json.JSONObject
-import reactor.core.publisher.Flux
-import reactor.core.publisher.Mono
-import reactor.function.TupleUtils
-import java.time.Instant
-import java.util.function.Predicate
-
-@Suppress("BlockingMethodInNonBlockingContext")
-object GoogleExternalAuthHandler {
- fun requestCode(event: MessageCreateEvent, settings: GuildSettings): Mono {
- return GoogleAuthWrapper.requestDeviceCode().flatMap { response ->
- if (response.code == GlobalVal.STATUS_SUCCESS) {
- //Got code -- Send message with code, start auth poll
- val successJson = JSONObject(response.body!!.string())
- response.body?.close()
- response.close()
-
- val embed = EmbedCreateSpec.builder()
- .author("DisCal", BotSettings.BASE_URL.get(), iconUrl)
- .title(Messages.getMessage("Embed.AddCalendar.Code.Title", settings))
- .addField(
- Messages.getMessage("Embed.AddCalendar.Code.Code", settings),
- successJson.getString("user_code"),
- true)
- .footer(Messages.getMessage("Embed.AddCalendar.Code.Footer", settings), null)
- .url(successJson.getString("verification_url"))
- .color(discalColor)
- .build()
-
- event.message.authorAsMember.flatMap { user ->
- val poll = ExternalGoogleAuthPoll(
- user,
- settings,
- interval = successJson.getInt("interval"),
- expiresIn = successJson.getInt("expires_in"),
- remainingSeconds = successJson.getInt("expires_in"),
- deviceCode = successJson.getString("device_code")
- ) { this.pollForAuth(it as ExternalGoogleAuthPoll) }
-
- Messages.sendDirectMessage(
- Messages.getMessage("AddCalendar.Auth.Code.Request.Success", settings),
- embed,
- user
- ).then(GoogleAuthWrapper.scheduleOAuthPoll(poll))
- }
- } else {
- //Bad response -- Log, send message
- val body = response.body?.string()
- response.body?.close()
- response.close()
- LOGGER.debug(DEFAULT, "Error request access token | Status code: ${response.code} | ${
- response
- .message
- } | $body")
-
- event.message.authorAsMember.flatMap {
- Messages.sendDirectMessage(
- Messages.getMessage("AddCalendar.Auth.Code.Request.Failure.NotOkay", settings), it)
- }
- }.then()
- }
- }
-
- private fun pollForAuth(poll: ExternalGoogleAuthPoll): Mono {
- return GoogleAuthWrapper.requestPollResponse(poll).flatMap { response ->
- when (response.code) {
- GlobalVal.STATUS_FORBIDDEN -> {
- //Handle access denied -- Send message, cancel poll
- Messages.sendDirectMessage(Messages.getMessage("AddCalendar.Auth.Poll.Failure.Deny", poll
- .settings), poll.user)
- .then(Mono.error(GoogleAuthCancelException()))
- }
- GlobalVal.STATUS_BAD_REQUEST, GlobalVal.STATUS_PRECONDITION_REQUIRED -> {
- //See if auth is pending, if so, just reschedule...
- val errorJson = JSONObject(response.body!!.string())
- response.body?.close()
- response.close()
- when {
- "authorization_pending".equals(errorJson.getString("error"), true) -> {
- //Response pending
- Mono.empty()
- }
- "expired_token".equals(errorJson.getString("error"), true) -> {
- //Token is expired -- Send message, cancel poll
- Messages.sendDirectMessage(Messages.getMessage("AddCal.Auth.Poll.Failure.Expired", poll
- .settings), poll.user)
- .then(Mono.error(GoogleAuthCancelException()))
- }
- else -> {
- //Unknown error -- Log, send message, cancel poll
- LOGGER.debug(DEFAULT, "[E.GCA] Poll failure", "Status: ${response.code} | ${response.message} | $errorJson")
-
- Messages.sendDirectMessage(Messages.getMessage("Notification.Error.Network", poll.settings),
- poll.user)
- .then(Mono.error(GoogleAuthCancelException()))
- }
- }
- }
- GlobalVal.STATUS_RATE_LIMITED -> {
- //We got rate limited. Oops. Let's just poll half as often.
- poll.interval = poll.interval * 2
-
- //Nothing else needs to be done for this to be handled
- Mono.empty()
- }
- GlobalVal.STATUS_SUCCESS -> {
- //Access granted -- Save creds, get calendars, list for user, cancel auth
- val successJson = JSONObject(response.body!!.string())
- response.body?.close()
- response.close()
-
- //Save creds
- val calData = CalendarData.emptyExternal(poll.settings.guildID, CalendarHost.GOOGLE)
- val encryption = AESEncryption(calData.privateKey)
-
-
- val accessMono = encryption.encrypt(successJson.getString("access_token"))
- val refreshMono = encryption.encrypt(successJson.getString("refresh_token"))
-
- Mono.zip(accessMono, refreshMono).flatMap(TupleUtils.function { access, refresh ->
- calData.encryptedAccessToken = access
- calData.encryptedRefreshToken = refresh
- calData.expiresAt = Instant.now().plusSeconds(successJson.getLong("expires_in"))
-
-
- DatabaseManager.updateCalendar(calData)
- .then(CalendarWrapper.getUsersExternalCalendars(calData))
- .flatMapMany { Flux.fromIterable(it) }
- .map { cal ->
- EmbedCreateSpec.builder()
- .author("DisCal", BotSettings.BASE_URL.get(), iconUrl)
- .title(Messages.getMessage("Embed.AddCalendar.List.Title", poll.settings))
- .addField(
- Messages.getMessage("Embed.AddCalendar.List.Name", poll.settings),
- cal.summary,
- false)
- .addField(
- Messages.getMessage("Embed.AddCalendar.List.TimeZone", poll.settings),
- cal.timeZone,
- false)
- .addField(
- Messages.getMessage("Embed.AddCalendar.List.ID", poll.settings),
- cal.id,
- false)
- .color(discalColor)
- .build()
- }.flatMap { Messages.sendDirectMessage(it, poll.user) }
- .switchIfEmpty {
- Messages.sendDirectMessage(
- Messages.getMessage("AddCalendar.Auth.Poll.Failure.ListCalendars", poll.settings),
- poll.user
- )
- }.then(Mono.error(GoogleAuthCancelException()))
- })
- }
- else -> {
- //Unknown error -- Log, send message, cancel poll
- LOGGER.debug(DEFAULT, "Network error | poll failure" +
- " | Status code: ${response.code} | ${response.message} | ${response.body?.string()}")
- response.body?.close()
- response.close()
-
- Messages.sendDirectMessage(
- Messages.getMessage("Notification.Error.Network", poll.settings), poll.user)
- .then(Mono.error(GoogleAuthCancelException()))
- }
- }
- }.onErrorResume(Predicate.not(GoogleAuthCancelException::class::isInstance)) {
- //Other error -- Log, send message, cancel poll
- LOGGER.error(DEFAULT, "Failed to poll for authorization to google account", it)
-
- Messages.sendDirectMessage(Messages.getMessage("Notification.Error.Unknown", poll.settings), poll.user)
- .then(Mono.error(GoogleAuthCancelException()))
- }
- }
-}
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/service/AnnouncementService.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/service/AnnouncementService.kt
index 3196c66d5..e0307ff89 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/service/AnnouncementService.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/service/AnnouncementService.kt
@@ -1,16 +1,14 @@
package org.dreamexposure.discal.client.service
import discord4j.common.util.Snowflake
+import discord4j.core.GatewayDiscordClient
import discord4j.core.`object`.entity.Guild
import discord4j.core.`object`.entity.Message
import discord4j.core.`object`.entity.channel.GuildMessageChannel
import discord4j.core.spec.MessageCreateSpec
import discord4j.rest.http.client.ClientException
import io.netty.handler.codec.http.HttpResponseStatus
-import org.dreamexposure.discal.client.DisCalClient
import org.dreamexposure.discal.client.message.embed.AnnouncementEmbed
-import org.dreamexposure.discal.core.`object`.announcement.Announcement
-import org.dreamexposure.discal.core.`object`.announcement.AnnouncementCache
import org.dreamexposure.discal.core.database.DatabaseManager
import org.dreamexposure.discal.core.entities.Calendar
import org.dreamexposure.discal.core.entities.Event
@@ -19,6 +17,8 @@ import org.dreamexposure.discal.core.enums.announcement.AnnouncementType.*
import org.dreamexposure.discal.core.extensions.discord4j.getCalendar
import org.dreamexposure.discal.core.extensions.messageContentSafe
import org.dreamexposure.discal.core.logger.LOGGER
+import org.dreamexposure.discal.core.`object`.announcement.Announcement
+import org.dreamexposure.discal.core.`object`.announcement.AnnouncementCache
import org.dreamexposure.discal.core.utils.GlobalVal
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
@@ -29,7 +29,9 @@ import java.time.Duration
import java.util.concurrent.ConcurrentHashMap
@Component
-class AnnouncementService : ApplicationRunner {
+class AnnouncementService(
+ private val discordClient: GatewayDiscordClient
+) : ApplicationRunner {
private val maxDifferenceMs = Duration.ofMinutes(5).toMillis()
private val cached = ConcurrentHashMap()
@@ -45,10 +47,7 @@ class AnnouncementService : ApplicationRunner {
// Runner
private fun doAnnouncementCycle(): Mono {
- //TODO: This should come in through DI once other legacy is removed/rewritten
- if (DisCalClient.client == null) return Mono.empty()
-
- return DisCalClient.client!!.guilds.flatMap { guild ->
+ return discordClient.guilds.flatMap { guild ->
DatabaseManager.getEnabledAnnouncements(guild.id).flatMapMany { Flux.fromIterable(it) }.flatMap { announcement ->
when (announcement.modifier) {
AnnouncementModifier.BEFORE -> handleBeforeModifier(guild, announcement)
@@ -108,12 +107,14 @@ class AnnouncementService : ApplicationRunner {
.flatMap { DatabaseManager.deleteAnnouncement(announcement.id) }
.then()
}
+
UNIVERSAL -> {
return getEvents(guild, announcement)
.filterWhen { isInRange(announcement, it) }
.flatMap { sendAnnouncement(guild, announcement, it) }
.then()
}
+
COLOR -> {
return getEvents(guild, announcement)
.filter { it.color == announcement.eventColor }
@@ -121,6 +122,7 @@ class AnnouncementService : ApplicationRunner {
.flatMap { sendAnnouncement(guild, announcement, it) }
.then()
}
+
RECUR -> {
return getEvents(guild, announcement)
.filter { it.eventId.contains("_") && it.eventId.split("_")[0] == announcement.eventId }
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/service/HeartbeatService.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/service/HeartbeatService.kt
index 80747f571..a97b88794 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/service/HeartbeatService.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/service/HeartbeatService.kt
@@ -1,15 +1,15 @@
package org.dreamexposure.discal.client.service
+import discord4j.core.GatewayDiscordClient
import kotlinx.serialization.encodeToString
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
-import org.dreamexposure.discal.client.DisCalClient
-import org.dreamexposure.discal.core.`object`.BotSettings
+import org.dreamexposure.discal.core.config.Config
+import org.dreamexposure.discal.core.logger.LOGGER
import org.dreamexposure.discal.core.`object`.network.discal.BotInstanceData
import org.dreamexposure.discal.core.`object`.rest.HeartbeatRequest
import org.dreamexposure.discal.core.`object`.rest.HeartbeatType
-import org.dreamexposure.discal.core.logger.LOGGER
import org.dreamexposure.discal.core.utils.GlobalVal
import org.dreamexposure.discal.core.utils.GlobalVal.HTTP_CLIENT
import org.dreamexposure.discal.core.utils.GlobalVal.JSON
@@ -23,20 +23,22 @@ import reactor.core.scheduler.Schedulers
import java.time.Duration
@Component
-class HeartbeatService : ApplicationRunner {
+class HeartbeatService(
+ private val discordClient: GatewayDiscordClient,
+) : ApplicationRunner {
+ private final val apiUrl = Config.URL_API.getString()
private fun heartbeat(): Mono {
- //TODO: Use DI for this soon
- return BotInstanceData.load(DisCalClient.client)
+ return BotInstanceData.load(discordClient)
.map { data ->
val requestBody = HeartbeatRequest(HeartbeatType.BOT, botInstanceData = data)
val body = JSON_FORMAT.encodeToString(requestBody).toRequestBody(JSON)
Request.Builder()
- .url("${BotSettings.API_URL.get()}/v2/status/heartbeat")
- .post(body)
- .header("Authorization", BotSettings.BOT_API_TOKEN.get())
+ .url("$apiUrl/v2/status/heartbeat")
+ .post(body)
+ .header("Authorization", Config.SECRET_DISCAL_API_KEY.getString())
.header("Content-Type", "application/json")
.build()
}.flatMap {
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/service/ShutdownHook.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/service/ShutdownHook.kt
new file mode 100644
index 000000000..d898b43da
--- /dev/null
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/service/ShutdownHook.kt
@@ -0,0 +1,18 @@
+package org.dreamexposure.discal.client.service
+
+import discord4j.core.GatewayDiscordClient
+import jakarta.annotation.PreDestroy
+import org.dreamexposure.discal.core.logger.LOGGER
+import org.dreamexposure.discal.core.utils.GlobalVal.STATUS
+import org.springframework.stereotype.Component
+
+// Required to be a component for shutdown hook due to lifecycle management of the discord client
+@Component
+class ShutdownHook(private val discordClient: GatewayDiscordClient) {
+ @PreDestroy
+ fun onShutdown() {
+ LOGGER.info(STATUS, "Shutting down shard")
+
+ discordClient.logout().subscribe()
+ }
+}
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/service/StaticMessageService.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/service/StaticMessageService.kt
index e3b8272a1..c591c0c4e 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/service/StaticMessageService.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/service/StaticMessageService.kt
@@ -1,20 +1,22 @@
package org.dreamexposure.discal.client.service
+import discord4j.core.GatewayDiscordClient
import discord4j.core.`object`.entity.Guild
import discord4j.core.spec.MessageEditSpec
import discord4j.rest.http.client.ClientException
import org.dreamexposure.discal.Application
import org.dreamexposure.discal.Application.Companion.getShardIndex
-import org.dreamexposure.discal.client.DisCalClient
import org.dreamexposure.discal.client.message.embed.CalendarEmbed
-import org.dreamexposure.discal.core.`object`.GuildSettings
-import org.dreamexposure.discal.core.`object`.StaticMessage
import org.dreamexposure.discal.core.database.DatabaseManager
import org.dreamexposure.discal.core.entities.Calendar
import org.dreamexposure.discal.core.extensions.discord4j.getCalendar
import org.dreamexposure.discal.core.extensions.discord4j.getSettings
import org.dreamexposure.discal.core.logger.LOGGER
+import org.dreamexposure.discal.core.`object`.GuildSettings
+import org.dreamexposure.discal.core.`object`.StaticMessage
import org.dreamexposure.discal.core.utils.GlobalVal.DEFAULT
+import org.springframework.beans.factory.BeanFactory
+import org.springframework.beans.factory.getBean
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
import org.springframework.stereotype.Component
@@ -26,8 +28,9 @@ import java.time.Instant
import java.time.temporal.ChronoUnit
@Component
-class StaticMessageService : ApplicationRunner {
- //TODO: use gateway client from DI once available
+class StaticMessageService(private val beanFactory: BeanFactory) : ApplicationRunner {
+ private val discordClient: GatewayDiscordClient
+ get() = beanFactory.getBean()
override fun run(args: ApplicationArguments?) {
Flux.interval(Duration.ofHours(1))
@@ -39,16 +42,14 @@ class StaticMessageService : ApplicationRunner {
private fun doMessageUpdateLogic(): Mono {
- if (DisCalClient.client == null) return Mono.empty()
-
- return DatabaseManager.getStaticMessagesForShard(Application.getShardCount(), getShardIndex().toInt())
+ return DatabaseManager.getStaticMessagesForShard(Application.getShardCount(), getShardIndex())
.flatMapMany { Flux.fromIterable(it) }
//We have no interest in updating the message so close to its last update
.filter { Duration.between(Instant.now(), it.lastUpdate).abs().toMinutes() >= 30 }
// Only update messages in range
.filter { Duration.between(Instant.now(), it.scheduledUpdate).toMinutes() <= 60 }
.flatMap { data ->
- DisCalClient.client!!.getMessageById(data.channelId, data.messageId).flatMap { message ->
+ discordClient.getMessageById(data.channelId, data.messageId).flatMap { message ->
when (data.type) {
StaticMessage.Type.CALENDAR_OVERVIEW -> {
val guildMono = message.guild.cache()
@@ -81,7 +82,7 @@ class StaticMessageService : ApplicationRunner {
}
fun updateStaticMessage(calendar: Calendar, settings: GuildSettings): Mono {
- return DisCalClient.client!!.getGuildById(settings.guildID)
+ return discordClient.getGuildById(settings.guildID)
.flatMap { updateStaticMessages(it, calendar, settings) }
}
diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/service/StatusChanger.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/service/StatusChanger.kt
index 0662b1185..cd05835e9 100644
--- a/client/src/main/kotlin/org/dreamexposure/discal/client/service/StatusChanger.kt
+++ b/client/src/main/kotlin/org/dreamexposure/discal/client/service/StatusChanger.kt
@@ -1,10 +1,10 @@
package org.dreamexposure.discal.client.service
+import discord4j.core.GatewayDiscordClient
import discord4j.core.`object`.presence.ClientActivity
import discord4j.core.`object`.presence.ClientPresence
import org.dreamexposure.discal.Application
import org.dreamexposure.discal.GitProperty
-import org.dreamexposure.discal.client.DisCalClient
import org.dreamexposure.discal.core.database.DatabaseManager
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
@@ -16,7 +16,9 @@ import java.time.Duration
import java.util.concurrent.atomic.AtomicInteger
@Component
-class StatusChanger: ApplicationRunner {
+class StatusChanger(
+ private val discordClient: GatewayDiscordClient,
+): ApplicationRunner {
private val index = AtomicInteger(0)
private val statuses = listOf(
@@ -33,10 +35,7 @@ class StatusChanger: ApplicationRunner {
)
private fun update(): Mono {
- if (DisCalClient.client == null)
- return Mono.empty()
-
- val guCountMono = DisCalClient.client!!.guilds.count()
+ val guCountMono = discordClient.guilds.count()
val calCountMono = DatabaseManager.getCalendarCount()
val annCountMono = DatabaseManager.getAnnouncementCount()
@@ -58,7 +57,7 @@ class StatusChanger: ApplicationRunner {
.replace("{version}", GitProperty.DISCAL_VERSION.value)
- DisCalClient.client!!.updatePresence(ClientPresence.online(ClientActivity.playing(status)))
+ discordClient.updatePresence(ClientPresence.online(ClientActivity.playing(status)))
})
}
diff --git a/client/src/main/resources/application.properties b/client/src/main/resources/application.properties
deleted file mode 100644
index 36f339d9b..000000000
--- a/client/src/main/resources/application.properties
+++ /dev/null
@@ -1,3 +0,0 @@
-spring.application.name=DisCal Client
-server.port=8080
-spring.session.store-type=redis
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index a72f8b477..37953ec68 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -3,29 +3,24 @@ import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeSpec
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+import java.util.*
plugins {
+ // Kotlin
kotlin("plugin.serialization")
+ id("org.jetbrains.kotlin.plugin.allopen")
+
+ // Spring
kotlin("plugin.spring")
+ id("io.spring.dependency-management")
+ // Tooling
id("com.gorylenko.gradle-git-properties")
- id("org.jetbrains.kotlin.plugin.allopen")
}
val discord4jVersion: String by properties
-val discord4jStoresVersion: String by properties
val kotlinSrcDir: File = buildDir.resolve("core/src/main/kotlin")
-dependencies {
- api("com.discord4j:discord4j-core:$discord4jVersion") {
- exclude(group = "io.projectreactor.netty", module = "*")
- }
- api("com.discord4j:stores-redis:$discord4jStoresVersion") {
- exclude(group = "io.netty", module = "*")
- exclude(group = "io.projectreactor.netty", module = "*")
- }
-}
-
kotlin {
sourceSets {
all {
@@ -43,6 +38,7 @@ gitProperties {
"$version.d${System.currentTimeMillis().div(1000)}" //Seconds since epoch
}
+ // Custom git properties for compile-time constants
customProperty("discal.version", versionName)
customProperty("discal.version.d4j", discord4jVersion)
}
@@ -52,7 +48,7 @@ tasks {
doLast {
@Suppress("UNCHECKED_CAST")
val gitProperties = ext[gitProperties.extProperty] as Map
- val enumPairs = gitProperties.mapKeys { it.key.replace('.', '_').toUpperCase() }
+ val enumPairs = gitProperties.mapKeys { it.key.replace('.', '_').uppercase(Locale.getDefault()) }
val enumBuilder = TypeSpec.enumBuilder("GitProperty")
.primaryConstructor(
diff --git a/core/src/main/java/org/dreamexposure/discal/core/file/ReadFile.java b/core/src/main/java/org/dreamexposure/discal/core/file/ReadFile.java
deleted file mode 100644
index fc350cbb0..000000000
--- a/core/src/main/java/org/dreamexposure/discal/core/file/ReadFile.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package org.dreamexposure.discal.core.file;
-
-import kotlin.text.Charsets;
-import org.dreamexposure.discal.core.utils.GlobalVal;
-import org.json.JSONObject;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.core.io.Resource;
-import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
-import org.springframework.util.FileCopyUtils;
-import reactor.core.publisher.Mono;
-import reactor.core.scheduler.Schedulers;
-
-import java.io.InputStreamReader;
-
-/**
- * Created by Nova Fox on 11/10/17.
- * Website: www.cloudcraftgaming.com
- * For Project: DisCal-Discord-Bot
- */
-@Deprecated
-public class ReadFile {
- private static final Logger LOGGER = LoggerFactory.getLogger(ReadFile.class);
-
- public static Mono readAllLangFiles() {
- return Mono.fromCallable(() -> {
- final JSONObject langs = new JSONObject();
-
- try {
- var pathMatching = new PathMatchingResourcePatternResolver();
-
- for (Resource res : pathMatching.getResources("languages/*.json")) {
- var reader = new InputStreamReader(res.getInputStream(), Charsets.UTF_8);
-
- // Open the file
- final String contents = FileCopyUtils.copyToString(reader);
-
- //Close reader
- reader.close();
-
- //Parse json
- final JSONObject json = new JSONObject(contents);
-
- if (!json.getString("Language").equalsIgnoreCase("TEMPLATE"))
- langs.put(json.getString("Language"), json);
- }
- } catch (final Exception e) {
- LOGGER.error(GlobalVal.getDEFAULT(), "Failed to load lang files", e);
- }
- return langs;
- }).subscribeOn(Schedulers.boundedElastic());
- }
-}
diff --git a/core/src/main/java/org/dreamexposure/discal/core/file/package-info.java b/core/src/main/java/org/dreamexposure/discal/core/file/package-info.java
deleted file mode 100644
index 04f464209..000000000
--- a/core/src/main/java/org/dreamexposure/discal/core/file/package-info.java
+++ /dev/null
@@ -1 +0,0 @@
-package org.dreamexposure.discal.core.file;
\ No newline at end of file
diff --git a/core/src/main/java/org/dreamexposure/discal/core/utils/GeneralUtils.java b/core/src/main/java/org/dreamexposure/discal/core/utils/GeneralUtils.java
deleted file mode 100644
index c9814a508..000000000
--- a/core/src/main/java/org/dreamexposure/discal/core/utils/GeneralUtils.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package org.dreamexposure.discal.core.utils;
-
-import java.util.List;
-import java.util.Random;
-
-/**
- * Created by Nova Fox on 11/10/17.
- * Website: www.cloudcraftgaming.com
- * For Project: DisCal-Discord-Bot
- */
-@Deprecated
-public class GeneralUtils {
- /**
- * Gets the contents of the message at a set offset.
- *
- * @param args The args of the command.
- * @param offset The offset in the string.
- * @return The contents of the message at a set offset.
- */
- public static String getContent(final List args, final int offset) {
- final StringBuilder content = new StringBuilder();
- for (int i = offset; i < args.size(); i++) {
- content.append(args.get(i)).append(" ");
- }
- return content.toString().trim();
- }
-
- /**
- * This is an overkill parser made by xaanit. You can thank him for this nightmare.
- *
- * regardless, it works, and therefore we will use it because generally speaking it seems some users do not understand that "<" and ">" are not in fact required and are just symbols CLEARLY DEFINED in our documentation.
- *
- * @param str The string to parse.
- * @return The string, but without the user errors.
- */
- @SuppressWarnings("MagicNumber")
- public static String overkillParser(final String str) {
- final Random random = new Random(str.length() * 2L >>> 4 & 3);
- final StringBuilder leftFace = new StringBuilder();
- final StringBuilder rightFace = new StringBuilder();
- final String alphabet = "abcdefghijklmnopqrstuvwxyz";
- for (int i = 0; i < 30; i++) {
- leftFace.append(alphabet.charAt(random.nextInt(alphabet.length())));
- rightFace.append(alphabet.charAt(random.nextInt(alphabet.length())));
- }
- return str.replace("<<", leftFace.toString()).replace(">>", rightFace.toString()).replace("<", "").replace(">", "").replace(leftFace.toString(), "<").replace(rightFace.toString(), ">");
- }
-}
diff --git a/core/src/main/java/org/dreamexposure/discal/core/utils/PermissionChecker.java b/core/src/main/java/org/dreamexposure/discal/core/utils/PermissionChecker.java
deleted file mode 100644
index 80efdf2f9..000000000
--- a/core/src/main/java/org/dreamexposure/discal/core/utils/PermissionChecker.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package org.dreamexposure.discal.core.utils;
-
-import discord4j.core.event.domain.message.MessageCreateEvent;
-import discord4j.core.object.entity.Member;
-import discord4j.rest.util.Permission;
-import reactor.core.publisher.Mono;
-
-/**
- * Created by Nova Fox on 1/19/17.
- * Website: www.cloudcraftgaming.com
- * For Project: DisCal
- */
-@Deprecated
-public class PermissionChecker {
- public static Mono hasManageServerRole(final MessageCreateEvent event) {
- return Mono.justOrEmpty(event.getMember())
- .flatMap(Member::getBasePermissions)
- .map(perms -> perms.contains(Permission.MANAGE_GUILD)
- || perms.contains(Permission.ADMINISTRATOR))
- .defaultIfEmpty(false);
- }
-}
diff --git a/core/src/main/java/org/dreamexposure/discal/core/utils/package-info.java b/core/src/main/java/org/dreamexposure/discal/core/utils/package-info.java
deleted file mode 100644
index 9e219f815..000000000
--- a/core/src/main/java/org/dreamexposure/discal/core/utils/package-info.java
+++ /dev/null
@@ -1 +0,0 @@
-package org.dreamexposure.discal.core.utils;
\ No newline at end of file
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/Application.kt b/core/src/main/kotlin/org/dreamexposure/discal/Application.kt
index a41b920aa..be394dfe8 100644
--- a/core/src/main/kotlin/org/dreamexposure/discal/Application.kt
+++ b/core/src/main/kotlin/org/dreamexposure/discal/Application.kt
@@ -1,20 +1,19 @@
package org.dreamexposure.discal
-import org.dreamexposure.discal.core.`object`.BotSettings
+import org.dreamexposure.discal.core.config.Config
import org.springframework.boot.autoconfigure.SpringBootApplication
-import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration
import org.springframework.boot.autoconfigure.session.SessionAutoConfiguration
import java.lang.management.ManagementFactory
import java.time.Duration
import java.util.*
import kotlin.math.roundToInt
-@SpringBootApplication(exclude = [SessionAutoConfiguration::class, R2dbcAutoConfiguration::class])
+@SpringBootApplication(exclude = [SessionAutoConfiguration::class])
class Application {
companion object {
val instanceId: UUID = UUID.randomUUID()
- fun getShardIndex(): String {
+ fun getShardIndex(): Int {
/*
This fucking sucks. So k8s doesn't expose the pod ordinal for a pod in a stateful set
https://github.com/kubernetes/kubernetes/pull/68719
@@ -31,17 +30,17 @@ class Application {
return if (shardPodName != null) {
//In k8s, parse this shit
val parts = shardPodName.split("-")
- parts[parts.size -1]
+ parts[parts.size -1].toInt()
} else {
//Fall back to config value
- BotSettings.SHARD_INDEX.get()
+ Config.SHARD_INDEX.getInt()
}
}
fun getShardCount(): Int {
val shardCount = System.getenv("SHARD_COUNT")
return shardCount?.toInt() ?: //Fall back to config
- BotSettings.SHARD_COUNT.get().toInt()
+ Config.SHARD_COUNT.getInt()
}
fun getUptime(): Duration {
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/business/ApiKeyService.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/business/ApiKeyService.kt
new file mode 100644
index 000000000..633f8d3b9
--- /dev/null
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/business/ApiKeyService.kt
@@ -0,0 +1,23 @@
+package org.dreamexposure.discal.core.business
+
+import kotlinx.coroutines.reactor.awaitSingleOrNull
+import org.dreamexposure.discal.core.database.ApiRepository
+import org.dreamexposure.discal.core.`object`.new.ApiKey
+import org.springframework.stereotype.Component
+
+
+@Component
+class DefaultApiKeyService(
+ private val apiRepository: ApiRepository,
+): ApiKeyService {
+ override suspend fun getKey(key: String): ApiKey? {
+ return apiRepository.getByApiKey(key)
+ .map(::ApiKey)
+ .awaitSingleOrNull()
+ }
+
+}
+
+interface ApiKeyService {
+ suspend fun getKey(key: String): ApiKey?
+}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/business/CalendarService.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/business/CalendarService.kt
new file mode 100644
index 000000000..85a24b008
--- /dev/null
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/business/CalendarService.kt
@@ -0,0 +1,66 @@
+package org.dreamexposure.discal.core.business
+
+import discord4j.common.util.Snowflake
+import kotlinx.coroutines.reactor.awaitSingle
+import kotlinx.coroutines.reactor.awaitSingleOrNull
+import org.dreamexposure.discal.CalendarCache
+import org.dreamexposure.discal.core.database.CalendarRepository
+import org.dreamexposure.discal.core.`object`.new.Calendar
+import org.springframework.stereotype.Component
+
+@Component
+class DefaultCalendarService(
+ private val calendarRepository: CalendarRepository,
+ private val calendarCache: CalendarCache,
+) : CalendarService {
+ override suspend fun getAllCalendars(guildId: Snowflake): List {
+ var calendars = calendarCache.get(guildId.asLong())?.toList()
+ if (calendars != null) return calendars
+
+ calendars = calendarRepository.findAllByGuildId(guildId.asLong())
+ .map(::Calendar)
+ .collectList()
+ .awaitSingle()
+
+ calendarCache.put(guildId.asLong(), calendars.toTypedArray())
+ return calendars
+ }
+
+ override suspend fun getCalendar(guildId: Snowflake, number: Int): Calendar? {
+ return getAllCalendars(guildId).first { it.number == number }
+ }
+
+ override suspend fun updateCalendar(calendar: Calendar) {
+ calendarRepository.updateCalendarByGuildIdAndCalendarNumber(
+ guildId = calendar.guildId.asLong(),
+ calendarNumber = calendar.number,
+ host = calendar.host.name,
+ calendarId = calendar.id,
+ calendarAddress = calendar.address,
+ external = calendar.external,
+ credentialId = calendar.secrets.credentialId,
+ privateKey = calendar.secrets.privateKey,
+ accessToken = calendar.secrets.encryptedAccessToken,
+ refreshToken = calendar.secrets.encryptedRefreshToken,
+ expiresAt = calendar.secrets.expiresAt.toEpochMilli(),
+ ).awaitSingleOrNull()
+
+ val cached = calendarCache.get(calendar.guildId.asLong())
+ if (cached != null) {
+ val newList = cached.toMutableList()
+ newList.removeIf { it.number == calendar.number }
+ calendarCache.put(calendar.guildId.asLong(), (newList + calendar).toTypedArray())
+ }
+ }
+
+}
+
+interface CalendarService {
+ // TODO: Need a function to invalidate cache because bot and API are using Db Manager
+
+ suspend fun getAllCalendars(guildId: Snowflake): List
+
+ suspend fun getCalendar(guildId: Snowflake, number: Int): Calendar?
+
+ suspend fun updateCalendar(calendar: Calendar)
+}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/business/CredentialService.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/business/CredentialService.kt
new file mode 100644
index 000000000..a345c715c
--- /dev/null
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/business/CredentialService.kt
@@ -0,0 +1,58 @@
+package org.dreamexposure.discal.core.business
+
+import kotlinx.coroutines.reactor.awaitSingle
+import kotlinx.coroutines.reactor.awaitSingleOrNull
+import org.dreamexposure.discal.CredentialsCache
+import org.dreamexposure.discal.core.database.CredentialData
+import org.dreamexposure.discal.core.database.CredentialsRepository
+import org.dreamexposure.discal.core.`object`.new.Credential
+import org.springframework.stereotype.Component
+
+
+@Component
+class DefaultCredentialService(
+ private val credentialsRepository: CredentialsRepository,
+ private val credentialsCache: CredentialsCache,
+) : CredentialService {
+
+ override suspend fun createCredential(credential: Credential): Credential {
+ val saved = credentialsRepository.save(CredentialData(
+ credentialNumber = credential.credentialNumber,
+ accessToken = credential.encryptedAccessToken,
+ refreshToken = credential.encryptedRefreshToken,
+ expiresAt = credential.expiresAt.toEpochMilli(),
+ )).map(::Credential).awaitSingle()
+
+ credentialsCache.put(saved.credentialNumber, saved)
+ return saved
+ }
+
+ override suspend fun getCredential(number: Int): Credential? {
+ var credential = credentialsCache.get(number)
+ if (credential != null) return credential
+
+ credential = credentialsRepository.findByCredentialNumber(number)
+ .map(::Credential)
+ .awaitSingle()
+
+ if (credential != null) credentialsCache.put(number, credential)
+ return credential
+ }
+
+ override suspend fun updateCredential(credential: Credential) {
+ credentialsRepository.updateByCredentialNumber(
+ credentialNumber = credential.credentialNumber,
+ refreshToken = credential.encryptedRefreshToken,
+ accessToken = credential.encryptedAccessToken,
+ expiresAt = credential.expiresAt.toEpochMilli(),
+ ).awaitSingleOrNull()
+
+ credentialsCache.put(credential.credentialNumber, credential)
+ }
+}
+
+interface CredentialService {
+ suspend fun createCredential(credential: Credential): Credential
+ suspend fun getCredential(number: Int): Credential?
+ suspend fun updateCredential(credential: Credential)
+}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/business/SessionService.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/business/SessionService.kt
new file mode 100644
index 000000000..2bfc9e4d5
--- /dev/null
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/business/SessionService.kt
@@ -0,0 +1,72 @@
+package org.dreamexposure.discal.core.business
+
+import discord4j.common.util.Snowflake
+import kotlinx.coroutines.reactor.awaitSingle
+import kotlinx.coroutines.reactor.awaitSingleOrNull
+import org.dreamexposure.discal.core.database.SessionData
+import org.dreamexposure.discal.core.database.SessionRepository
+import org.dreamexposure.discal.core.`object`.WebSession
+import org.springframework.stereotype.Component
+import java.time.Instant
+
+
+@Component
+class DefaultSessionService(
+ private val sessionRepository: SessionRepository,
+) : SessionService {
+ // TODO: I do want to add caching, but need to figure out how I want to do that
+
+ override suspend fun createSession(session: WebSession): WebSession {
+ return sessionRepository.save(SessionData(
+ token = session.token,
+ userId = session.user.asLong(),
+ expiresAt = session.expiresAt,
+ accessToken = session.accessToken,
+ refreshToken = session.refreshToken,
+ )).map(::WebSession).awaitSingle()
+ }
+
+ override suspend fun getSession(token: String): WebSession? {
+ return sessionRepository.findByToken(token)
+ .map(::WebSession)
+ .awaitSingleOrNull()
+ }
+
+ override suspend fun getSessions(userId: Snowflake): List {
+ return sessionRepository.findAllByUserId(userId.asLong())
+ .map(::WebSession)
+ .collectList()
+ .awaitSingle()
+ }
+
+ override suspend fun deleteSession(token: String) {
+ sessionRepository.deleteByToken(token).awaitSingleOrNull()
+ }
+
+ override suspend fun deleteAllSessions(userId: Snowflake) {
+ sessionRepository.deleteAllByUserId(userId.asLong()).awaitSingleOrNull()
+ }
+
+ override suspend fun deleteExpiredSessions() {
+ sessionRepository.deleteAllByExpiresAtIsLessThan(Instant.now()).awaitSingleOrNull()
+ }
+}
+
+interface SessionService {
+ suspend fun createSession(session: WebSession): WebSession
+
+ suspend fun getSession(token: String): WebSession?
+
+ suspend fun getSessions(userId: Snowflake): List
+ suspend fun deleteSession(token: String)
+
+ suspend fun deleteAllSessions(userId: Snowflake)
+
+ suspend fun deleteExpiredSessions()
+
+ suspend fun removeAndInsertSession(session: WebSession): WebSession {
+ deleteAllSessions(session.user)
+
+ return createSession(session)
+ }
+}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/cache/CacheRepository.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/cache/CacheRepository.kt
new file mode 100644
index 000000000..3ad6f39c0
--- /dev/null
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/cache/CacheRepository.kt
@@ -0,0 +1,17 @@
+package org.dreamexposure.discal.core.cache
+
+import java.time.Duration
+
+interface CacheRepository {
+
+ val ttl: Duration
+ get() = Duration.ofMinutes(60)
+
+ suspend fun put(key: K, value: V)
+
+ suspend fun get(key: K): V?
+
+ suspend fun getAndRemove(key: K): V?
+
+ suspend fun evict(key: K)
+}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/cache/DiscalCache.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/cache/DiscalCache.kt
index 8a1981023..b93e65ebc 100644
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/cache/DiscalCache.kt
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/cache/DiscalCache.kt
@@ -1,13 +1,14 @@
package org.dreamexposure.discal.core.cache
import discord4j.common.util.Snowflake
-import org.dreamexposure.discal.core.`object`.GuildSettings
import org.dreamexposure.discal.core.entities.Calendar
+import org.dreamexposure.discal.core.`object`.GuildSettings
import reactor.core.publisher.Flux
import java.time.Duration
import java.util.concurrent.ConcurrentHashMap
//TODO: Eventually use redis instead of in-memory so these can be shared across the whole discal network and need less time for eventual consistency.
+@Deprecated("Use proper caching impl")
object DiscalCache {
//guild id -> settings
val guildSettings: MutableMap = ConcurrentHashMap()
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/cache/JdkCacheRepository.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/cache/JdkCacheRepository.kt
new file mode 100644
index 000000000..944c5dc3b
--- /dev/null
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/cache/JdkCacheRepository.kt
@@ -0,0 +1,46 @@
+package org.dreamexposure.discal.core.cache
+
+import org.dreamexposure.discal.core.extensions.isExpiredTtl
+import reactor.core.publisher.Flux
+import java.time.Duration
+import java.time.Instant
+import java.util.concurrent.ConcurrentHashMap
+
+class JdkCacheRepository(override val ttl: Duration) : CacheRepository {
+ private val cache = ConcurrentHashMap>()
+
+ init {
+ Flux.interval(Duration.ofMinutes(5))
+ .map { evictOld() }
+ .subscribe()
+ }
+
+ override suspend fun put(key: K, value: V) {
+ cache[key] = Pair(Instant.now(), value)
+ }
+
+ override suspend fun get(key: K): V? {
+ val cached = cache[key] ?: return null
+ if (Instant.now().isAfter(cached.first)) {
+ evict(key)
+ return null
+ }
+ return cached.second
+ }
+
+ override suspend fun getAndRemove(key: K): V? {
+ val cached = cache[key] ?: return null
+ evict(key)
+
+ return if (cached.first.isExpiredTtl()) null else cached.second
+
+ }
+
+ override suspend fun evict(key: K) {
+ cache.remove(key)
+ }
+
+ private fun evictOld() {
+ cache.forEach { (key, pair) -> if (Duration.between(pair.first, Instant.now()) >= ttl) cache.remove(key) }
+ }
+}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/cache/RedisCacheRepository.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/cache/RedisCacheRepository.kt
new file mode 100644
index 000000000..1ea1764b1
--- /dev/null
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/cache/RedisCacheRepository.kt
@@ -0,0 +1,44 @@
+package org.dreamexposure.discal.core.cache
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import org.springframework.data.redis.cache.RedisCacheManager
+
+class RedisCacheRepository(
+ private val valueType: Class,
+ redisCacheManager: RedisCacheManager,
+ private val mapper: ObjectMapper,
+ cacheName: String,
+) : CacheRepository {
+ private val cache = redisCacheManager.getCache(cacheName)!!
+
+ override suspend fun put(key: K, value: V) {
+ mapper.writer()
+ cache.put(key, mapper.writeValueAsString(value))
+
+ }
+
+ override suspend fun get(key: K): V? {
+ val raw = cache.get(key, String::class.java)
+ return if (raw != null) mapper.readValue(raw, valueType) else null
+ }
+
+ override suspend fun getAndRemove(key: K): V? {
+ val raw = cache.get(key, String::class.java)
+ val parsed = if (raw != null) mapper.readValue(raw, valueType) else null
+
+ evict(key)
+ return parsed
+ }
+
+ override suspend fun evict(key: K) {
+ cache.evictIfPresent(key)
+ }
+
+ companion object {
+ inline operator fun invoke(
+ redisCacheManager: RedisCacheManager,
+ mapper: ObjectMapper,
+ cacheName: String,
+ ) = RedisCacheRepository(V::class.java, redisCacheManager, mapper, cacheName)
+ }
+}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/config/BeanConfig.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/config/BeanConfig.kt
new file mode 100644
index 000000000..be5272076
--- /dev/null
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/config/BeanConfig.kt
@@ -0,0 +1,31 @@
+package org.dreamexposure.discal.core.config
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
+import com.fasterxml.jackson.module.kotlin.registerKotlinModule
+import discord4j.common.JacksonResources
+import org.dreamexposure.discal.core.serializers.DurationMapper
+import org.dreamexposure.discal.core.serializers.SnowflakeMapper
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Primary
+import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping
+
+@Configuration
+class BeanConfig {
+ @Bean
+ @Primary
+ fun objectMapper(): ObjectMapper {
+ // Use d4j's object mapper
+ return JacksonResources.create().objectMapper
+ .registerKotlinModule()
+ .registerModule(JavaTimeModule())
+ .registerModule(SnowflakeMapper())
+ .registerModule(DurationMapper())
+ }
+
+ @Bean
+ fun handlerMapping(): RequestMappingHandlerMapping {
+ return RequestMappingHandlerMapping()
+ }
+}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/config/CacheConfig.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/config/CacheConfig.kt
new file mode 100644
index 000000000..66e6fdad1
--- /dev/null
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/config/CacheConfig.kt
@@ -0,0 +1,77 @@
+package org.dreamexposure.discal.core.config
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import org.dreamexposure.discal.CalendarCache
+import org.dreamexposure.discal.CredentialsCache
+import org.dreamexposure.discal.OauthStateCache
+import org.dreamexposure.discal.core.cache.JdkCacheRepository
+import org.dreamexposure.discal.core.cache.RedisCacheRepository
+import org.dreamexposure.discal.core.extensions.asMinutes
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Primary
+import org.springframework.data.redis.cache.RedisCacheConfiguration
+import org.springframework.data.redis.cache.RedisCacheManager
+import org.springframework.data.redis.connection.RedisConnectionFactory
+
+@Configuration
+class CacheConfig {
+ // Cache name constants
+ private val prefix = Config.CACHE_PREFIX.getString()
+ private val settingsCacheName = "$prefix.settingsCache"
+ private val credentialsCacheName = "$prefix.credentialsCache"
+ private val oauthStateCacheName = "$prefix.oauthStateCache"
+ private val calendarCacheName = "$prefix.calendarCache"
+
+ private val settingsTtl = Config.CACHE_TTL_SETTINGS_MINUTES.getLong().asMinutes()
+ private val credentialsTll = Config.CACHE_TTL_CREDENTIALS_MINUTES.getLong().asMinutes()
+ private val oauthStateTtl = Config.CACHE_TTL_OAUTH_STATE_MINUTES.getLong().asMinutes()
+ private val calendarTtl = Config.CACHE_TTL_CALENDAR_MINUTES.getLong().asMinutes()
+
+
+ // Redis caching
+ @Bean
+ @ConditionalOnProperty("bot.cache.redis", havingValue = "true")
+ fun redisCache(connection: RedisConnectionFactory): RedisCacheManager {
+ return RedisCacheManager.builder(connection)
+ .withCacheConfiguration(settingsCacheName,
+ RedisCacheConfiguration.defaultCacheConfig().entryTtl(settingsTtl)
+ ).withCacheConfiguration(credentialsCacheName,
+ RedisCacheConfiguration.defaultCacheConfig().entryTtl(credentialsTll)
+ ).withCacheConfiguration(oauthStateCacheName,
+ RedisCacheConfiguration.defaultCacheConfig().entryTtl(oauthStateTtl)
+ ).withCacheConfiguration(calendarCacheName,
+ RedisCacheConfiguration.defaultCacheConfig().entryTtl(calendarTtl)
+ ).build()
+ }
+
+ @Bean
+ @Primary
+ @ConditionalOnProperty("bot.cache.redis", havingValue = "true")
+ fun credentialsRedisCache(cacheManager: RedisCacheManager, objectMapper: ObjectMapper): CredentialsCache =
+ RedisCacheRepository(cacheManager, objectMapper, credentialsCacheName)
+
+ @Bean
+ @Primary
+ @ConditionalOnProperty("bot.cache.redis", havingValue = "true")
+ fun oauthStateRedisCache(cacheManager: RedisCacheManager, objectMapper: ObjectMapper): OauthStateCache =
+ RedisCacheRepository(cacheManager, objectMapper, oauthStateCacheName)
+
+ @Bean
+ @Primary
+ @ConditionalOnProperty("bot.cache.redis", havingValue = "true")
+ fun calendarRedisCache(cacheManager: RedisCacheManager, objectMapper: ObjectMapper): CalendarCache =
+ RedisCacheRepository(cacheManager, objectMapper, calendarCacheName)
+
+
+ // In-memory fallback caching
+ @Bean
+ fun credentialsFallbackCache(): CredentialsCache = JdkCacheRepository(settingsTtl)
+
+ @Bean
+ fun oauthStateFallbackCache(): OauthStateCache = JdkCacheRepository(settingsTtl)
+
+ @Bean
+ fun calendarFallbackCache(): CalendarCache = JdkCacheRepository(calendarTtl)
+}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/config/Config.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/config/Config.kt
new file mode 100644
index 000000000..334b64070
--- /dev/null
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/config/Config.kt
@@ -0,0 +1,87 @@
+package org.dreamexposure.discal.core.config
+
+import java.io.FileReader
+import java.util.*
+
+enum class Config(private val key: String, private var value: Any? = null) {
+ // Basic spring settings
+ APP_NAME("spring.application.name"),
+
+ // Database settings, to be removed once DatabaseManager is retired
+ SQL_URL("spring.r2dbc.url"),
+ SQL_USERNAME("spring.r2dbc.username"),
+ SQL_PASSWORD("spring.r2dbc.password"),
+
+ // Redis cache settings
+ REDIS_HOST("spring.data.redis.host"),
+ REDIS_PORT("spring.data.redis.port"),
+ REDIS_PASSWORD("spring.data.redis.password", ""),
+ CACHE_REDIS_IS_CLUSTER("redis.cluster", false),
+ CACHE_USE_REDIS("bot.cache.redis", false),
+
+ CACHE_PREFIX("bot.cache.prefix", "discal"),
+ CACHE_TTL_SETTINGS_MINUTES("bot.cache.ttl-minutes.settings", 60),
+ CACHE_TTL_CREDENTIALS_MINUTES("bot.cache.ttl-minutes.credentials", 120),
+ CACHE_TTL_ACCOUNTS_MINUTES("bot.cache.ttl-minutes.accounts", 60),
+ CACHE_TTL_OAUTH_STATE_MINUTES("bot.cache.ttl-minutes.oauth.state", 5),
+ CACHE_TTL_CALENDAR_MINUTES("bots.cache.ttl-minutes.calendar", 120),
+
+ // Security configuration
+
+ // Bot secrets
+ SECRET_DISCAL_API_KEY("bot.secret.api-token"),
+ SECRET_BOT_TOKEN("bot.secret.token"),
+ SECRET_CLIENT_SECRET("bot.secret.client-secret"),
+
+ SECRET_GOOGLE_CLIENT_ID("bot.secret.google.client.id"),
+ SECRET_GOOGLE_CLIENT_SECRET("bot.secret.google.client.secret"),
+ SECRET_GOOGLE_CREDENTIAL_KEY("bot.secret.google.credential.key"),
+ SECRET_GOOGLE_CREDENTIAL_COUNT("bot.secret.google.credential.count"),
+
+ SECRET_WEBHOOK_DEFAULT("bot.secret.default-webhook"),
+ SECRET_WEBHOOK_STATUS("bot.secret.status-webhook"),
+
+ SECRET_INTEGRATION_D_BOTS_GG_TOKEN("bot.secret.token.d-bots-gg"),
+ SECRET_INTEGRATION_TOP_GG_TOKEN("bot.secret.token.top-gg"),
+
+ // Various URLs
+ URL_BASE("bot.url.base"),
+ URL_API("bot.url.api"),
+ URL_CAM("bot.url.cam"),
+ URL_SUPPORT("bot.url.support", "https://discord.gg/2TFqyuy"),
+ URL_INVITE("bot.url.invite"),
+ URL_DISCORD_REDIRECT("bot.url.discord.redirect"),
+
+ // Everything else
+ SHARD_COUNT("bot.sharding.count"),
+ SHARD_INDEX("bot.sharding.index", 0),
+ RESTART_SERVICE_ENABLED("bot.services.restart", false),
+ HEARTBEAT_INTERVAL("bot.timing.heartbeat.seconds", 120),
+
+ DISCORD_APP_ID("bot.discord-app-id"),
+
+ LOGGING_WEBHOOKS_USE("bot.logging.webhooks.use", false),
+ LOGGING_WEBHOOKS_ALL_ERRORS("bot.logging.webhooks.all-error", false),
+
+ INTEGRATIONS_UPDATE_BOT_LIST_SITES("bot.integrations.update-bot-sites", false),
+
+
+ ;
+
+ companion object {
+ fun init() {
+ val props = Properties()
+ props.load(FileReader("application.properties"))
+
+ entries.forEach { it.value = props.getProperty(it.key, it.value?.toString()) }
+ }
+ }
+
+ fun getString() = value.toString()
+
+ fun getInt() = getString().toInt()
+
+ fun getLong() = getString().toLong()
+
+ fun getBoolean() = getString().toBoolean()
+}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/database/ApiData.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/database/ApiData.kt
new file mode 100644
index 000000000..8fa2de651
--- /dev/null
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/database/ApiData.kt
@@ -0,0 +1,11 @@
+package org.dreamexposure.discal.core.database
+
+import org.springframework.data.relational.core.mapping.Table
+
+@Table("api")
+data class ApiData(
+ val userId: String,
+ val apiKey: String,
+ val blocked: Boolean,
+ val timeIssued: Long,
+)
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/database/ApiRepository.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/database/ApiRepository.kt
new file mode 100644
index 000000000..41852fff1
--- /dev/null
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/database/ApiRepository.kt
@@ -0,0 +1,8 @@
+package org.dreamexposure.discal.core.database
+
+import org.springframework.data.r2dbc.repository.R2dbcRepository
+import reactor.core.publisher.Mono
+
+interface ApiRepository: R2dbcRepository {
+ fun getByApiKey(apiKey: String): Mono
+}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/database/CalendarData.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/database/CalendarData.kt
new file mode 100644
index 000000000..4e895c714
--- /dev/null
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/database/CalendarData.kt
@@ -0,0 +1,18 @@
+package org.dreamexposure.discal.core.database
+
+import org.springframework.data.relational.core.mapping.Table
+
+@Table("calendars")
+data class CalendarData(
+ val guildId: Long,
+ val calendarNumber: Int,
+ val host: String,
+ val calendarId: String,
+ val calendarAddress: String,
+ val external: Boolean,
+ val credentialId: Int,
+ val privateKey: String,
+ val accessToken: String,
+ val refreshToken: String,
+ val expiresAt: Long,
+)
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/database/CalendarRepository.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/database/CalendarRepository.kt
new file mode 100644
index 000000000..6274fb86d
--- /dev/null
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/database/CalendarRepository.kt
@@ -0,0 +1,40 @@
+package org.dreamexposure.discal.core.database
+
+import org.springframework.data.r2dbc.repository.Query
+import org.springframework.data.r2dbc.repository.R2dbcRepository
+import reactor.core.publisher.Flux
+import reactor.core.publisher.Mono
+
+interface CalendarRepository : R2dbcRepository {
+
+ fun findAllByGuildId(guildId: Long): Flux
+
+ fun findByGuildIdAndCalendarNumber(guildId: Long, calendarNumber: Int): Mono
+
+ @Query("""
+ UPDATE calendars
+ SET host = :host,
+ calendar_id = :calendarId,
+ calendar_address = :calendarAddress,
+ external = :external,
+ credential_id = :credentialId,
+ private_key = :privateKey,
+ access_token = :accessToken,
+ refresh_token = :refreshToken,
+ expires_at = :expiresAt
+ WHERE guild_id = :guildId AND calendar_number = :calendarNumber
+ """)
+ fun updateCalendarByGuildIdAndCalendarNumber(
+ guildId: Long,
+ calendarNumber: Int,
+ host: String,
+ calendarId: String,
+ calendarAddress: String,
+ external: Boolean,
+ credentialId: Int,
+ privateKey: String,
+ accessToken: String,
+ refreshToken: String,
+ expiresAt: Long,
+ ): Mono
+}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/database/CredentialData.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/database/CredentialData.kt
new file mode 100644
index 000000000..e567b911e
--- /dev/null
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/database/CredentialData.kt
@@ -0,0 +1,11 @@
+package org.dreamexposure.discal.core.database
+
+import org.springframework.data.relational.core.mapping.Table
+
+@Table("credentials")
+data class CredentialData(
+ val credentialNumber: Int,
+ val refreshToken: String,
+ val accessToken: String,
+ val expiresAt: Long,
+)
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/database/CredentialsRepository.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/database/CredentialsRepository.kt
new file mode 100644
index 000000000..55f4bad90
--- /dev/null
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/database/CredentialsRepository.kt
@@ -0,0 +1,24 @@
+package org.dreamexposure.discal.core.database
+
+import org.springframework.data.r2dbc.repository.Query
+import org.springframework.data.r2dbc.repository.R2dbcRepository
+import reactor.core.publisher.Mono
+
+interface CredentialsRepository : R2dbcRepository {
+
+ fun findByCredentialNumber(credentialNumber: Int): Mono
+
+ @Query("""
+ UPDATE credentials
+ SET refresh_token = :refreshToken,
+ access_token = :accessToken,
+ expires_at = :expiresAt
+ WHERE credential_number = :credentialNumber
+ """)
+ fun updateByCredentialNumber(
+ credentialNumber: Int,
+ refreshToken: String,
+ accessToken: String,
+ expiresAt: Long,
+ ): Mono
+}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/database/DatabaseManager.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/database/DatabaseManager.kt
index 3ba56519f..b32bd440f 100644
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/database/DatabaseManager.kt
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/database/DatabaseManager.kt
@@ -10,6 +10,7 @@ import io.r2dbc.spi.ConnectionFactories
import io.r2dbc.spi.ConnectionFactoryOptions.*
import io.r2dbc.spi.Result
import org.dreamexposure.discal.core.cache.DiscalCache
+import org.dreamexposure.discal.core.config.Config
import org.dreamexposure.discal.core.enums.announcement.AnnouncementModifier
import org.dreamexposure.discal.core.enums.announcement.AnnouncementStyle
import org.dreamexposure.discal.core.enums.announcement.AnnouncementType
@@ -19,15 +20,12 @@ import org.dreamexposure.discal.core.enums.time.TimeFormat
import org.dreamexposure.discal.core.extensions.asStringList
import org.dreamexposure.discal.core.extensions.setFromString
import org.dreamexposure.discal.core.logger.LOGGER
-import org.dreamexposure.discal.core.`object`.BotSettings
import org.dreamexposure.discal.core.`object`.GuildSettings
import org.dreamexposure.discal.core.`object`.StaticMessage
-import org.dreamexposure.discal.core.`object`.WebSession
import org.dreamexposure.discal.core.`object`.announcement.Announcement
import org.dreamexposure.discal.core.`object`.calendar.CalendarData
import org.dreamexposure.discal.core.`object`.event.EventData
import org.dreamexposure.discal.core.`object`.event.RsvpData
-import org.dreamexposure.discal.core.`object`.google.GoogleCredentialData
import org.dreamexposure.discal.core.`object`.web.UserAPIAccount
import org.dreamexposure.discal.core.utils.GlobalVal.DEFAULT
import org.intellij.lang.annotations.Language
@@ -45,11 +43,9 @@ object DatabaseManager {
builder()
.option(DRIVER, "pool")
.option(PROTOCOL, "mysql")
- .option(HOST, BotSettings.SQL_HOST.get())
- .option(PORT, BotSettings.SQL_PORT.get().toInt())
- .option(USER, BotSettings.SQL_USER.get())
- .option(PASSWORD, BotSettings.SQL_PASS.get())
- .option(DATABASE, BotSettings.SQL_DB.get())
+ .from(parse(Config.SQL_URL.getString()))
+ .option(USER, Config.SQL_USERNAME.getString())
+ .option(PASSWORD, Config.SQL_PASSWORD.getString())
.build()
)
@@ -129,7 +125,7 @@ object DatabaseManager {
CONTROL_ROLE = ?, ANNOUNCEMENT_STYLE = ?, TIME_FORMAT = ?,
LANG = ?, PREFIX = ?, PATRON_GUILD = ?, DEV_GUILD = ?,
MAX_CALENDARS = ?, DM_ANNOUNCEMENTS = ?,
- BRANDED = ? WHERE GUILD_ID = ?
+ BRANDED = ?, event_keep_duration = ? WHERE GUILD_ID = ?
""".trimMargin()
Mono.from(
@@ -144,7 +140,8 @@ object DatabaseManager {
.bind(7, settings.maxCalendars)
.bind(8, settings.getDmAnnouncementsString())
.bind(9, settings.branded)
- .bind(10, settings.guildID.asLong())
+ .bind(10, settings.eventKeepDuration)
+ .bind(11, settings.guildID.asLong())
.execute()
).flatMap { res -> Mono.from(res.rowsUpdated) }
.hasElement()
@@ -152,8 +149,8 @@ object DatabaseManager {
} else {
val insertCommand = """INSERT INTO ${Tables.GUILD_SETTINGS}
(GUILD_ID, CONTROL_ROLE, ANNOUNCEMENT_STYLE, TIME_FORMAT, LANG, PREFIX,
- PATRON_GUILD, DEV_GUILD, MAX_CALENDARS, DM_ANNOUNCEMENTS, BRANDED)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ PATRON_GUILD, DEV_GUILD, MAX_CALENDARS, DM_ANNOUNCEMENTS, BRANDED, event_keep_duration)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""".trimMargin()
Mono.from(
@@ -169,6 +166,7 @@ object DatabaseManager {
.bind(8, settings.maxCalendars)
.bind(9, settings.getDmAnnouncementsString())
.bind(10, settings.branded)
+ .bind(11, settings.eventKeepDuration)
.execute()
).flatMap { res -> Mono.from(res.rowsUpdated) }
.hasElement()
@@ -458,53 +456,6 @@ object DatabaseManager {
}
}
- fun updateCredentialData(credData: GoogleCredentialData): Mono {
- return connect { c ->
- Mono.from(
- c.createStatement(Queries.SELECT_CREDENTIAL_DATA)
- .bind(0, credData.credentialNumber)
- .execute()
- ).flatMapMany { res ->
- res.map { row, _ -> row }
- }.hasElements().flatMap { exists ->
- if (exists) {
- val updateCommand = """UPDATE ${Tables.CREDS} SET
- REFRESH_TOKEN = ?, ACCESS_TOKEN = ?, EXPIRES_AT = ?
- WHERE CREDENTIAL_NUMBER = ?""".trimMargin()
-
- Mono.from(
- c.createStatement(updateCommand)
- .bind(0, credData.encryptedRefreshToken)
- .bind(1, credData.encryptedAccessToken)
- .bind(2, credData.expiresAt.toEpochMilli())
- .bind(3, credData.credentialNumber)
- .execute()
- ).flatMapMany(Result::getRowsUpdated)
- .hasElements()
- .thenReturn(true)
- } else {
- val insertCommand = """INSERT INTO ${Tables.CREDS}
- |(CREDENTIAL_NUMBER, REFRESH_TOKEN, ACCESS_TOKEN, EXPIRES_AT)
- |VALUES(?, ?, ?, ?)""".trimMargin()
-
- Mono.from(
- c.createStatement(insertCommand)
- .bind(0, credData.credentialNumber)
- .bind(1, credData.encryptedRefreshToken)
- .bind(2, credData.encryptedAccessToken)
- .bind(3, credData.expiresAt.toEpochMilli())
- .execute()
- ).flatMapMany(Result::getRowsUpdated)
- .hasElements()
- .thenReturn(true)
- }.doOnError {
- LOGGER.error(DEFAULT, "Failed to update credential data", it)
- }.onErrorResume { Mono.just(false) }
- }
-
- }
- }
-
fun getAPIAccount(APIKey: String): Mono {
return connect { c ->
Mono.from(
@@ -550,10 +501,11 @@ object DatabaseManager {
val maxCals = row["MAX_CALENDARS", Int::class.java]!!
val dmAnnouncementsString = row["DM_ANNOUNCEMENTS", String::class.java]!!
val branded = row["BRANDED", Boolean::class.java]!!
+ val eventKeepDuration = row["event_keep_duration", Boolean::class.java]!!
val settings = GuildSettings(
guildId, controlRole, announcementStyle, timeFormat,
- lang, prefix, patron, dev, maxCals, branded
+ lang, prefix, patron, dev, maxCals, branded, eventKeepDuration,
)
settings.dmAnnouncements.setFromString(dmAnnouncementsString)
@@ -1096,29 +1048,6 @@ object DatabaseManager {
}.defaultIfEmpty(-1)
}
- fun getCredentialData(credNumber: Int): Mono {
- return connect { c ->
- Mono.from(
- c.createStatement(Queries.SELECT_CREDENTIAL_DATA)
- .bind(0, credNumber)
- .execute()
- ).flatMapMany { res ->
- res.map { row, _ ->
- val refresh = row["REFRESH_TOKEN", String::class.java]!!
- val access = row["ACCESS_TOKEN", String::class.java]!!
- val expires = Instant.ofEpochMilli(row["EXPIRES_AT", Long::class.java]!!)
-
- GoogleCredentialData(credNumber, refresh, access, expires)
- }
- }.next().retryWhen(Retry.max(3)
- .filter(IllegalStateException::class::isInstance)
- .filter { it.message != null && it.message!!.contains("Request queue was disposed") }
- ).doOnError {
- LOGGER.error(DEFAULT, "Failed to get enabled announcements by type", it)
- }.onErrorResume { Mono.empty() }
- }
- }
-
fun deleteAnnouncement(announcementId: String): Mono {
return connect { c ->
Mono.from(
@@ -1511,144 +1440,6 @@ object DatabaseManager {
}.collectList()
}
}
-
- /* Session Data */
- fun insertSessionData(session: WebSession): Mono {
- return connect { c ->
- Mono.from(
- c.createStatement(Queries.INSERT_SESSION_DATA)
- .bind(0, session.token)
- .bind(1, session.user.asLong())
- .bind(2, session.expiresAt)
- .bind(3, session.accessToken)
- .bind(4, session.refreshToken)
- .execute()
- ).flatMapMany(Result::getRowsUpdated)
- .hasElements()
- .thenReturn(true)
- }.doOnError {
- LOGGER.error(DEFAULT, "Failed to insert session data", it)
- }.onErrorResume { Mono.just(false) }
- }
-
- fun removeAndInsertSessionData(session: WebSession): Mono {
- return connect { c ->
- Mono.from(
- c.createStatement(Queries.REMOVE_AND_INSERT_SESSION_DATA)
- // Remove all existing sessions for user bindings
- .bind(0, session.user.asLong())
- // Insert new session bindings
- .bind(1, session.token)
- .bind(2, session.user.asLong())
- .bind(3, session.expiresAt)
- .bind(4, session.accessToken)
- .bind(5, session.refreshToken)
- .execute()
- ).flatMapMany(Result::getRowsUpdated)
- .hasElements()
- .thenReturn(true)
- }.doOnError {
- LOGGER.error(DEFAULT, "Failed to insert session data", it)
- }.onErrorResume { Mono.just(false) }
- }
-
- fun getSessionData(token: String): Mono {
- return connect { c ->
- Mono.from(
- c.createStatement(Queries.SELECT_SESSION_TOKEN)
- .bind(0, token)
- .execute()
- ).flatMapMany { res ->
- res.map { row, _ ->
- val userId = Snowflake.of(row["user_id", Long::class.java]!!)
- val expiresAt = row["expires_at", Instant::class.java]!!
- val accessToken = row["access_token", String::class.java]!!
- val refreshToken = row["refresh_token", String::class.java]!!
-
- WebSession(token, userId, expiresAt, accessToken, refreshToken)
- }
- }.next().retryWhen(Retry.max(3)
- .filter(IllegalStateException::class::isInstance)
- .filter { it.message != null && it.message!!.contains("Request queue was disposed") }
- ).doOnError {
- LOGGER.error(DEFAULT, "Failed to get session data by token", it)
- }.onErrorResume {
- Mono.empty()
- }
- }
- }
-
- fun getAllSessionsForUser(userId: Snowflake): Mono> {
- return connect { c ->
- Mono.from(
- c.createStatement(Queries.SELECT_SESSIONS_USER)
- .bind(0, userId.asLong())
- .execute()
- ).flatMapMany { res ->
- res.map { row, _ ->
- val token = row["token", String::class.java]!!
- val expiresAt = row["expires_at", Instant::class.java]!!
- val accessToken = row["access_token", String::class.java]!!
- val refreshToken = row["refresh_token", String::class.java]!!
-
- WebSession(token, userId, expiresAt, accessToken, refreshToken)
- }
- }.retryWhen(Retry.max(3)
- .filter(IllegalStateException::class::isInstance)
- .filter { it.message != null && it.message!!.contains("Request queue was disposed") }
- ).doOnError {
- LOGGER.error(DEFAULT, "Failed to get sessions for user", it)
- }.onErrorResume {
- Mono.empty()
- }.collectList()
- }
- }
-
- fun deleteSession(token: String): Mono {
- return connect { c ->
- Mono.from(
- c.createStatement(Queries.DELETE_SESSION)
- .bind(0, token)
- .execute()
- ).flatMapMany(Result::getRowsUpdated)
- .hasElements()
- .thenReturn(true)
- .doOnError {
- LOGGER.error(DEFAULT, "session delete failure", it)
- }.onErrorReturn(false)
- }.defaultIfEmpty(true) // If nothing was updated and no error was emitted, it's safe to return this worked.
- }
-
- fun deleteAllSessionsForUser(userId: Snowflake): Mono {
- return connect { c ->
- Mono.from(
- c.createStatement(Queries.DELETE_SESSIONS_FOR_USER)
- .bind(0, userId.asLong())
- .execute()
- ).flatMapMany(Result::getRowsUpdated)
- .hasElements()
- .thenReturn(true)
- .doOnError {
- LOGGER.error(DEFAULT, "delete all sessions for user failure", it)
- }.onErrorReturn(false)
- }.defaultIfEmpty(true) // If nothing was updated and no error was emitted, it's safe to return this worked.
- }
-
- fun deleteExpiredSessions(): Mono {
- return connect { c ->
- Mono.from(
- c.createStatement(Queries.DELETE_EXPIRED_SESSIONS)
- .bind(0, Instant.now()) // Delete everything that expired before now
- .execute()
- ).flatMapMany(Result::getRowsUpdated)
- .hasElements()
- .thenReturn(true)
- .doOnError {
- // Technically because we have handling for expired tokens we don't need to panic if this breaks
- LOGGER.error(DEFAULT, "Expired session delete failure", it)
- }.onErrorReturn(false)
- }.defaultIfEmpty(true) // If nothing was updated and no error was emitted, it's safe to return this worked.
- }
}
private object Queries {
@@ -1737,11 +1528,6 @@ private object Queries {
@Language("MySQL")
val SELECT_ALL_ANNOUNCEMENT_COUNT = """SELECT COUNT(*) FROM ${Tables.ANNOUNCEMENTS}"""
- @Language("MySQL")
- val SELECT_CREDENTIAL_DATA = """SELECT * FROM ${Tables.CREDS}
- WHERE CREDENTIAL_NUMBER = ?
- """.trimMargin()
-
@Language("MySQL")
val DELETE_ANNOUNCEMENT = """DELETE FROM ${Tables.ANNOUNCEMENTS}
WHERE ANNOUNCEMENT_ID = ?
@@ -1869,41 +1655,6 @@ private object Queries {
WHERE MOD(guild_id >> 22, ?) = ?
""".trimMargin()
- /* Session Data */
-
- @Language("MySQL")
- val INSERT_SESSION_DATA = """INSERT INTO ${Tables.SESSIONS}
- (token, user_id, expires_at, access_token, refresh_token)
- VALUES(?, ?, ?, ?, ?)
- """.trimMargin()
-
- @Language("MySQL")
- val SELECT_SESSION_TOKEN = """SELECT * FROM ${Tables.SESSIONS}
- WHERE token = ?
- """.trimMargin()
-
- @Language("MySQL")
- val SELECT_SESSIONS_USER = """SELECT * FROM ${Tables.SESSIONS}
- WHERE user_id = ?
- """.trimMargin()
-
- @Language("MySQL")
- val DELETE_SESSION = """DELETE FROM ${Tables.SESSIONS}
- WHERE token = ?
- """.trimMargin()
-
- @Language("MySQL")
- val DELETE_SESSIONS_FOR_USER = """DELETE FROM ${Tables.SESSIONS}
- WHERE user_id = ?
- """.trimMargin()
-
- val REMOVE_AND_INSERT_SESSION_DATA = "$DELETE_SESSIONS_FOR_USER;$INSERT_SESSION_DATA"
-
- @Language("MySQL")
- val DELETE_EXPIRED_SESSIONS = """DELETE FROM ${Tables.SESSIONS}
- where expires_at < ?
- """.trimMargin()
-
/* Delete everything */
@Language("MySQL")
@@ -1938,12 +1689,6 @@ private object Tables {
@Language("Kotlin")
const val RSVP = "rsvp"
- @Language("Kotlin")
- const val CREDS = "credentials"
-
@Language("Kotlin")
const val STATIC_MESSAGES = "static_messages"
-
- @Language("Kotlin")
- const val SESSIONS = "sessions"
}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/database/SessionData.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/database/SessionData.kt
new file mode 100644
index 000000000..17983b91e
--- /dev/null
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/database/SessionData.kt
@@ -0,0 +1,13 @@
+package org.dreamexposure.discal.core.database
+
+import org.springframework.data.relational.core.mapping.Table
+import java.time.Instant
+
+@Table("sessions")
+data class SessionData(
+ val token: String,
+ val userId: Long,
+ val expiresAt: Instant,
+ val accessToken: String,
+ val refreshToken: String,
+)
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/database/SessionRepository.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/database/SessionRepository.kt
new file mode 100644
index 000000000..eda0794bf
--- /dev/null
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/database/SessionRepository.kt
@@ -0,0 +1,18 @@
+package org.dreamexposure.discal.core.database
+
+import org.springframework.data.r2dbc.repository.R2dbcRepository
+import reactor.core.publisher.Flux
+import reactor.core.publisher.Mono
+import java.time.Instant
+
+interface SessionRepository : R2dbcRepository {
+ fun findByToken(token: String): Mono
+
+ fun findAllByUserId(userId: Long): Flux
+
+ fun deleteByToken(token: String): Mono
+
+ fun deleteAllByUserId(userId: Long): Mono
+
+ fun deleteAllByExpiresAtIsLessThan(expiresAt: Instant): Mono
+}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/entities/Calendar.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/entities/Calendar.kt
index ef069da50..acb90595b 100644
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/entities/Calendar.kt
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/entities/Calendar.kt
@@ -2,12 +2,12 @@ package org.dreamexposure.discal.core.entities
import discord4j.common.util.Snowflake
import discord4j.core.`object`.entity.Guild
+import org.dreamexposure.discal.core.config.Config
import org.dreamexposure.discal.core.entities.google.GoogleCalendar
import org.dreamexposure.discal.core.entities.response.UpdateCalendarResponse
import org.dreamexposure.discal.core.entities.spec.create.CreateEventSpec
import org.dreamexposure.discal.core.entities.spec.update.UpdateCalendarSpec
import org.dreamexposure.discal.core.enums.calendar.CalendarHost
-import org.dreamexposure.discal.core.`object`.BotSettings
import org.dreamexposure.discal.core.`object`.calendar.CalendarData
import org.dreamexposure.discal.core.`object`.web.WebCalendar
import org.json.JSONObject
@@ -83,7 +83,7 @@ interface Calendar {
* A link to view the calendar on the official discal website
*/
val link: String
- get() = "${BotSettings.BASE_URL.get()}/embed/${guildId.asString()}/calendar/$calendarNumber"
+ get() = "${Config.URL_BASE.getString()}/embed/${guildId.asString()}/calendar/$calendarNumber"
/**
* A link to view the calendar on the host's website (e.g. google.com)
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/entities/Event.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/entities/Event.kt
index 2b6bab189..fb37abc68 100644
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/entities/Event.kt
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/entities/Event.kt
@@ -181,6 +181,8 @@ interface Event {
* @return Whether the event is multi-day
*/
fun isMultiDay(): Boolean {
+ if (isAllDay()) return false // All day events should not count as multi-day events
+
val start = this.start.atZone(timezone).truncatedTo(ChronoUnit.DAYS)
val end = this.end.atZone(timezone).truncatedTo(ChronoUnit.DAYS)
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/entities/google/DisCalGoogleCredential.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/entities/google/DisCalGoogleCredential.kt
deleted file mode 100644
index 1e3af401b..000000000
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/entities/google/DisCalGoogleCredential.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package org.dreamexposure.discal.core.entities.google
-
-import org.dreamexposure.discal.core.`object`.BotSettings
-import org.dreamexposure.discal.core.`object`.google.GoogleCredentialData
-import org.dreamexposure.discal.core.crypto.AESEncryption
-import reactor.core.publisher.Mono
-import java.time.Instant
-
-data class DisCalGoogleCredential(
- val credentialData: GoogleCredentialData,
-) {
- private val aes: AESEncryption = AESEncryption(BotSettings.CREDENTIALS_KEY.get())
- private var access: String? = null
- private var refresh: String? = null
-
- fun getRefreshToken(): Mono {
- if (refresh != null) return Mono.justOrEmpty(refresh)
- return aes.decrypt(credentialData.encryptedRefreshToken)
- .doOnNext { refresh = it }
- }
-
- fun getAccessToken(): Mono {
- if (access != null) return Mono.justOrEmpty(access)
- return aes.decrypt(credentialData.encryptedAccessToken)
- .doOnNext { access = it }
- }
-
- fun setRefreshToken(token: String): Mono {
- refresh = token
- //credentialData.encryptedRefreshToken = aes.encrypt(token)
- return aes.encrypt(token)
- .doOnNext { credentialData.encryptedRefreshToken = it }
- .then()
- }
-
- fun setAccessToken(token: String): Mono {
- access = token
- //credentialData.encryptedAccessToken = aes.encrypt(token)
- return aes.encrypt(token)
- .doOnNext { credentialData.encryptedAccessToken = it }
- .then()
- }
-
- fun expired() = Instant.now().isAfter(credentialData.expiresAt)
-}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/extensions/InstantExtension.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/extensions/InstantExtension.kt
index 3025fc355..5b730f1c0 100644
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/extensions/InstantExtension.kt
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/extensions/InstantExtension.kt
@@ -21,3 +21,5 @@ fun Instant.humanReadableDate(timezone: ZoneId, format: TimeFormat, long: Boolea
fun Instant.humanReadableTime(timezone: ZoneId, format: TimeFormat): String {
return DateTimeFormatter.ofPattern(format.time).withZone(timezone).format(this)
}
+
+fun Instant.isExpiredTtl(): Boolean = Instant.now().isAfter(this)
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/extensions/Long.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/extensions/Long.kt
new file mode 100644
index 000000000..0e25b2b9e
--- /dev/null
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/extensions/Long.kt
@@ -0,0 +1,13 @@
+package org.dreamexposure.discal.core.extensions
+
+import discord4j.common.util.Snowflake
+import java.time.Duration
+import java.time.Instant
+
+fun Long.asSnowflake(): Snowflake = Snowflake.of(this)
+
+fun Long.asInstantMilli(): Instant = Instant.ofEpochMilli(this)
+
+fun Long.asSeconds(): Duration = Duration.ofSeconds(this)
+
+fun Long.asMinutes(): Duration = Duration.ofMinutes(this)
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/extensions/MutableList.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/extensions/MutableList.kt
index e077262c5..769e8ef62 100644
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/extensions/MutableList.kt
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/extensions/MutableList.kt
@@ -9,7 +9,7 @@ import java.time.temporal.TemporalAdjusters
import java.util.stream.Collectors
-fun MutableList.asStringList(): String {
+fun List.asStringList(): String {
val builder = StringBuilder()
for ((i, str) in this.withIndex()) {
@@ -94,12 +94,12 @@ fun MutableList.groupByDateMulti(): Map> {
val multi = mutableMapOf>()
sortedDates.forEach {
- val range = LongRange(it.toEpochSecond(), it.plusHours(23).plusMinutes(59).toEpochSecond())
+ val range = LongRange(it.toEpochSecond(), it.plusHours(23).plusMinutes(59).plusSeconds(59).toEpochSecond())
val events = mutableListOf()
this.forEach { event ->
- // When we check event end, we bump it back a second in order to prevent weirdness.
- if (range.contains(event.start.epochSecond) || range.contains(event.end.epochSecond - 1)) {
+ // When we check event end, we bump it back a couple seconds in order to prevent weirdness.
+ if (range.contains(event.start.epochSecond) || range.contains(event.end.epochSecond - 2)) {
// Event in range, add to list
events.add(event)
} else if (event.start.epochSecond < range.first && event.end.epochSecond > range.last) {
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/extensions/String.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/extensions/String.kt
index e34c1838d..fb9fae2a0 100644
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/extensions/String.kt
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/extensions/String.kt
@@ -5,6 +5,7 @@ import org.dreamexposure.discal.core.utils.GlobalVal
import org.dreamexposure.discal.core.utils.ImageValidator
import org.jsoup.Jsoup
import java.time.ZoneId
+import java.util.*
fun String.embedTitleSafe(): String = this.substring(0, (256).coerceAtMost(this.length))
@@ -58,3 +59,16 @@ fun String.padCenter(length: Int, padChar: Char = ' '): String {
}
return String(output)
}
+
+// TODO: Do db migration so this can be removed
+fun String.asLocale(): Locale {
+ return when (this) {
+ "ENGLISH" -> Locale.ENGLISH
+ "JAPANESE" -> Locale.JAPANESE
+ "PORTUGUESE" -> Locale.forLanguageTag("pt")
+ "SPANISH" -> Locale.forLanguageTag("es")
+ else -> Locale.ENGLISH
+ }
+}
+
+fun String.asStringListFromDatabase(): List = this.split(",").filter(String::isNotBlank)
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/logger/DiscordWebhookAppender.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/logger/DiscordWebhookAppender.kt
index 11db9c7f9..9fc6d39de 100644
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/logger/DiscordWebhookAppender.kt
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/logger/DiscordWebhookAppender.kt
@@ -1,5 +1,6 @@
package org.dreamexposure.discal.core.logger
+import ch.qos.logback.classic.Level
import ch.qos.logback.classic.spi.ILoggingEvent
import ch.qos.logback.core.AppenderBase
import club.minnced.discord.webhook.WebhookClient
@@ -9,31 +10,55 @@ import org.dreamexposure.discal.Application
import org.dreamexposure.discal.GitProperty
import org.dreamexposure.discal.core.extensions.embedDescriptionSafe
import org.dreamexposure.discal.core.extensions.embedFieldSafe
-import org.dreamexposure.discal.core.`object`.BotSettings
import org.dreamexposure.discal.core.utils.GlobalVal
import org.dreamexposure.discal.core.utils.GlobalVal.DEFAULT
import org.dreamexposure.discal.core.utils.GlobalVal.STATUS
+import java.io.FileReader
import java.time.Instant
+import java.util.*
class DiscordWebhookAppender : AppenderBase() {
private val defaultHook: WebhookClient?
private val statusHook: WebhookClient?
+ private val useWebhooks: Boolean
+ private val allErrorsWebhook: Boolean
+ private val appName: String
init {
- if (BotSettings.USE_WEBHOOKS.get().equals("true", true)) {
- defaultHook = WebhookClient.withUrl(BotSettings.DEFAULT_WEBHOOK.get())
- statusHook = WebhookClient.withUrl(BotSettings.STATUS_WEBHOOK.get())
+ val appProps = Properties()
+ appProps.load(FileReader("application.properties"))
+
+ useWebhooks = appProps.getProperty("bot.logging.webhooks.use", "false").toBoolean()
+ appName = appProps.getProperty("spring.application.name")
+
+ if (useWebhooks) {
+ defaultHook = WebhookClient.withUrl(appProps.getProperty("bot.secret.default-webhook"))
+ statusHook = WebhookClient.withUrl(appProps.getProperty("bot.secret.status-webhook"))
+ allErrorsWebhook = appProps.getProperty("bot.logging.webhooks.all-errors", "false").toBoolean()
} else {
defaultHook = null
statusHook = null
+ allErrorsWebhook = false
}
}
override fun append(eventObject: ILoggingEvent) {
- if (BotSettings.USE_WEBHOOKS.get().equals("true", true)) {
- when {
- eventObject.marker.equals(DEFAULT) -> executeDefault(eventObject)
- eventObject.marker.equals(STATUS) -> executeStatus(eventObject)
+ if (!useWebhooks) return
+
+ when {
+ eventObject.level.equals(Level.ERROR) && allErrorsWebhook -> {
+ executeDefault(eventObject)
+ return
+ }
+
+ eventObject.markerList.contains(STATUS) -> {
+ executeStatus(eventObject)
+ return
+ }
+
+ eventObject.markerList.contains(DEFAULT) -> {
+ executeDefault(eventObject)
+ return
}
}
}
@@ -41,13 +66,14 @@ class DiscordWebhookAppender : AppenderBase() {
private fun executeStatus(event: ILoggingEvent) {
val content = WebhookEmbedBuilder()
.setTitle(EmbedTitle("Status", null))
- .addField(EmbedField(true, "Shard Index", Application.getShardIndex()))
+ .addField(EmbedField(true, "Application", appName))
+ .addField(EmbedField(true, "Index", "${Application.getShardIndex()}"))
.addField(EmbedField(true, "Time", ""))
.addField(EmbedField(false, "Logger", event.loggerName.embedFieldSafe()))
.addField(EmbedField(true, "Level", event.level.levelStr))
.addField(EmbedField(true, "Thread", event.threadName.embedFieldSafe()))
.setDescription(event.formattedMessage.embedDescriptionSafe())
- .setColor(GlobalVal.discalColor.rgb)
+ .setColor(getEmbedColor(event))
.setFooter(EmbedFooter("v${GitProperty.DISCAL_VERSION.value}", null))
.setTimestamp(Instant.now())
@@ -62,13 +88,14 @@ class DiscordWebhookAppender : AppenderBase() {
private fun executeDefault(event: ILoggingEvent) {
val content = WebhookEmbedBuilder()
.setTitle(EmbedTitle(event.level.levelStr, null))
- .addField(EmbedField(true, "Shard Index", Application.getShardIndex()))
+ .addField(EmbedField(true, "Application", appName))
+ .addField(EmbedField(true, "Index", "${Application.getShardIndex()}"))
.addField(EmbedField(true, "Time", ""))
.addField(EmbedField(false, "Logger", event.loggerName.embedFieldSafe()))
.addField(EmbedField(true, "Level", event.level.levelStr))
.addField(EmbedField(true, "Thread", event.threadName.embedFieldSafe()))
.setDescription(event.formattedMessage.embedDescriptionSafe())
- .setColor(GlobalVal.discalColor.rgb)
+ .setColor(getEmbedColor(event))
.setFooter(EmbedFooter("v${GitProperty.DISCAL_VERSION.value}", null))
.setTimestamp(Instant.now())
@@ -81,4 +108,9 @@ class DiscordWebhookAppender : AppenderBase() {
}
+ private fun getEmbedColor(event: ILoggingEvent): Int {
+ return if (event.level.equals(Level.ERROR) || event.throwableProxy != null) GlobalVal.errorColor.rgb
+ else if (event.level.equals(Level.WARN)) GlobalVal.warnColor.rgb
+ else GlobalVal.discalColor.rgb
+ }
}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/object/BotSettings.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/object/BotSettings.kt
deleted file mode 100644
index ace6fface..000000000
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/object/BotSettings.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-package org.dreamexposure.discal.core.`object`
-
-import java.util.*
-
-enum class BotSettings {
- SQL_HOST,
- SQL_PORT,
- SQL_USER,
- SQL_PASS,
- SQL_DB,
- SQL_PREFIX,
-
- REDIS_HOSTNAME,
- REDIS_PORT,
- REDIS_PASSWORD,
- REDIS_USE_PASSWORD,
-
- TOKEN,
- SECRET,
- ID,
-
- GOOGLE_CLIENT_ID,
- GOOGLE_CLIENT_SECRET,
- CREDENTIALS_COUNT,
- CREDENTIALS_KEY,
-
- SHARD_COUNT,
- SHARD_INDEX,
-
- D_BOTS_GG_TOKEN,
- TOP_GG_TOKEN,
-
- TIME_OUT,
-
- REDIR_URI,
- REDIR_URL,
-
- INVITE_URL,
- SUPPORT_INVITE,
-
- BASE_URL,
- API_URL,
- CAM_URL,
-
- DEFAULT_WEBHOOK,
- STATUS_WEBHOOK,
-
- USE_REDIS_STORES,
- USE_WEBHOOKS,
- UPDATE_SITES,
- USE_RESTART_SERVICE,
-
- BOT_API_TOKEN,
-
- PROFILE;
-
- private var value: String? = null
-
- companion object {
- fun init(properties: Properties) {
- values().forEach {
- try {
- it.value = properties.getProperty(it.name)
- } catch (npe: NullPointerException) {
- throw IllegalStateException("Settings not valid! Check console for more information", npe)
- }
- }
- }
- }
-
- fun get() = this.value!!
-}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/object/GuildSettings.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/object/GuildSettings.kt
index 1dc7c2d30..7071f57df 100644
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/object/GuildSettings.kt
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/object/GuildSettings.kt
@@ -11,29 +11,32 @@ import java.util.*
@Serializable
data class GuildSettings(
- @Serializable(with = SnowflakeAsStringSerializer::class)
+ @Serializable(with = SnowflakeAsStringSerializer::class)
@SerialName("guild_id")
val guildID: Snowflake,
- @SerialName("control_role")
+ @SerialName("control_role")
var controlRole: String = "everyone",
- @SerialName("announcement_style")
+ @SerialName("announcement_style")
var announcementStyle: AnnouncementStyle = AnnouncementStyle.EVENT,
- @SerialName("time_format")
+ @SerialName("time_format")
var timeFormat: TimeFormat = TimeFormat.TWENTY_FOUR_HOUR,
- var lang: String = "ENGLISH",
- var prefix: String = "!",
+ var lang: String = "ENGLISH",
+ var prefix: String = "!",
- @SerialName("patron_guild")
- var patronGuild: Boolean = false,
- @SerialName("dev_guild")
- var devGuild: Boolean = false,
- @SerialName("max_calendars")
- var maxCalendars: Int = 1,
+ @SerialName("patron_guild")
+ var patronGuild: Boolean = false,
+ @SerialName("dev_guild")
+ var devGuild: Boolean = false,
+ @SerialName("max_calendars")
+ var maxCalendars: Int = 1,
- var branded: Boolean = false,
+ var branded: Boolean = false,
+
+ @SerialName("event_keep_duration")
+ var eventKeepDuration: Boolean = false,
) {
@SerialName("dm_announcements")
val dmAnnouncements: MutableList = mutableListOf()
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/object/WebSession.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/object/WebSession.kt
index 75adcea0d..36940e2af 100644
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/object/WebSession.kt
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/object/WebSession.kt
@@ -1,6 +1,8 @@
package org.dreamexposure.discal.core.`object`
import discord4j.common.util.Snowflake
+import org.dreamexposure.discal.core.database.SessionData
+import org.dreamexposure.discal.core.extensions.asSnowflake
import java.time.Instant
import java.time.temporal.ChronoUnit
@@ -14,4 +16,12 @@ data class WebSession(
val accessToken: String,
val refreshToken: String,
-)
+) {
+ constructor(data: SessionData) : this(
+ token = data.token,
+ user = data.userId.asSnowflake(),
+ expiresAt = data.expiresAt,
+ accessToken = data.accessToken,
+ refreshToken = data.refreshToken,
+ )
+}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/object/event/RsvpData.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/object/event/RsvpData.kt
index e0eb5dcff..c007c085b 100644
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/object/event/RsvpData.kt
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/object/event/RsvpData.kt
@@ -12,8 +12,7 @@ import discord4j.discordjson.json.MessageData
import discord4j.rest.http.client.ClientException
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
-import org.dreamexposure.discal.core.`object`.BotSettings
-import org.dreamexposure.discal.core.`object`.GuildSettings
+import org.dreamexposure.discal.core.config.Config
import org.dreamexposure.discal.core.entities.Event
import org.dreamexposure.discal.core.enums.time.DiscordTimestampFormat.LONG_DATETIME
import org.dreamexposure.discal.core.extensions.asDiscordTimestamp
@@ -22,6 +21,7 @@ import org.dreamexposure.discal.core.extensions.discord4j.getSettings
import org.dreamexposure.discal.core.extensions.embedFieldSafe
import org.dreamexposure.discal.core.extensions.toMarkdown
import org.dreamexposure.discal.core.logger.LOGGER
+import org.dreamexposure.discal.core.`object`.GuildSettings
import org.dreamexposure.discal.core.serializers.SnowflakeAsStringSerializer
import org.dreamexposure.discal.core.utils.GlobalVal
import org.dreamexposure.discal.core.utils.getEmbedMessage
@@ -238,7 +238,7 @@ data class RsvpData(
val builder = EmbedCreateSpec.builder()
// Even without branding enabled, we want the user to know what guild this is because it's in DMs
- .author(guild.name(), BotSettings.BASE_URL.get(), iconUrl)
+ .author(guild.name(), Config.URL_BASE.getString(), iconUrl)
.title(getEmbedMessage("rsvp", "waitlist.title", settings))
.description(getEmbedMessage("rsvp", "waitlist.desc", settings, userId, event.name))
.addField(
@@ -270,7 +270,7 @@ data class RsvpData(
val builder = EmbedCreateSpec.builder()
// Even without branding enabled, we want the user to know what guild this is because it's in DMs
- .author(guild.name(), BotSettings.BASE_URL.get(), iconUrl)
+ .author(guild.name(), Config.URL_BASE.getString(), iconUrl)
.title(getEmbedMessage("rsvp", "waitlist.title", settings))
.description(getEmbedMessage("rsvp", "waitlist.desc", settings, userId, event.name))
.addField(
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/object/google/ClientData.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/object/google/ClientData.kt
deleted file mode 100644
index 31f3d6017..000000000
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/object/google/ClientData.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package org.dreamexposure.discal.core.`object`.google
-
-import kotlinx.serialization.Serializable
-
-@Serializable
-data class ClientData(val clientId: String, val clientSecret: String)
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/object/network/discal/BotInstanceData.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/object/network/discal/BotInstanceData.kt
index 0980cb1fd..ca76912f4 100644
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/object/network/discal/BotInstanceData.kt
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/object/network/discal/BotInstanceData.kt
@@ -1,5 +1,6 @@
package org.dreamexposure.discal.core.`object`.network.discal
+import com.fasterxml.jackson.annotation.JsonProperty
import discord4j.core.GatewayDiscordClient
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -10,12 +11,15 @@ import reactor.core.publisher.Mono
@Serializable
data class BotInstanceData private constructor(
@SerialName("instance")
+ @JsonProperty("instance")
val instanceData: InstanceData,
@SerialName("shard_index")
+ @JsonProperty("shard_index")
val shardIndex: Int,
@SerialName("shard_count")
+ @JsonProperty("shard_count")
val shardCount: Int,
val guilds: Int = 0,
@@ -29,7 +33,7 @@ data class BotInstanceData private constructor(
.map { guildCount ->
BotInstanceData(
instanceData = InstanceData(),
- shardIndex = Application.getShardIndex().toInt(),
+ shardIndex = Application.getShardIndex(),
shardCount = Application.getShardCount(),
guilds = guildCount
)
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/object/network/discal/InstanceData.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/object/network/discal/InstanceData.kt
index f46102f31..0c18ae611 100644
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/object/network/discal/InstanceData.kt
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/object/network/discal/InstanceData.kt
@@ -1,5 +1,6 @@
package org.dreamexposure.discal.core.`object`.network.discal
+import com.fasterxml.jackson.annotation.JsonProperty
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.dreamexposure.discal.Application
@@ -15,17 +16,20 @@ import java.time.format.DateTimeFormatter
@Serializable
data class InstanceData(
@SerialName("instance_id")
+ @JsonProperty("instance_id")
val instanceId: String = Application.instanceId.toString(),
val version: String = GitProperty.DISCAL_VERSION.value,
@SerialName("d4j_version")
+ @JsonProperty("d4j_version")
val d4jVersion: String = GitProperty.DISCAL_VERSION_D4J.value,
@Serializable(with = DurationAsStringSerializer::class)
val uptime: Duration = Application.getUptime(),
@SerialName("last_heartbeat")
+ @JsonProperty("last_heartbeat")
@Serializable(with = InstantAsStringSerializer::class)
val lastHeartbeat: Instant = Instant.now(),
@@ -41,6 +45,7 @@ data class InstanceData(
}
@SerialName("human_uptime")
+ @JsonProperty("human_uptime")
@Suppress("unused") //Used in thymeleaf status page
var humanReadableUptime: String = uptime.getHumanReadable()
private set
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/ApiKey.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/ApiKey.kt
new file mode 100644
index 000000000..031290889
--- /dev/null
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/ApiKey.kt
@@ -0,0 +1,20 @@
+package org.dreamexposure.discal.core.`object`.new
+
+import discord4j.common.util.Snowflake
+import org.dreamexposure.discal.core.database.ApiData
+import org.dreamexposure.discal.core.extensions.asInstantMilli
+import java.time.Instant
+
+data class ApiKey(
+ val userId: Snowflake,
+ val key: String,
+ val blocked: Boolean,
+ val timeIssued: Instant,
+) {
+ constructor(data: ApiData): this(
+ userId = Snowflake.of(data.apiKey),
+ key = data.apiKey,
+ blocked = data.blocked,
+ timeIssued = data.timeIssued.asInstantMilli(),
+ )
+}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/Calendar.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/Calendar.kt
new file mode 100644
index 000000000..0205cba54
--- /dev/null
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/Calendar.kt
@@ -0,0 +1,42 @@
+package org.dreamexposure.discal.core.`object`.new
+
+import discord4j.common.util.Snowflake
+import org.dreamexposure.discal.core.database.CalendarData
+import org.dreamexposure.discal.core.enums.calendar.CalendarHost
+import org.dreamexposure.discal.core.extensions.asInstantMilli
+import org.dreamexposure.discal.core.extensions.asSnowflake
+import java.time.Instant
+
+data class Calendar(
+ val guildId: Snowflake,
+ val number: Int,
+ val host: CalendarHost,
+ val id: String,
+ val address: String,
+ val external: Boolean,
+ val secrets: Secrets,
+) {
+ constructor(data: CalendarData) : this(
+ guildId = data.guildId.asSnowflake(),
+ number = data.calendarNumber,
+ host = CalendarHost.valueOf(data.host),
+ id = data.calendarId,
+ address = data.calendarAddress,
+ external = data.external,
+ secrets = Secrets(
+ credentialId = data.credentialId,
+ privateKey = data.privateKey,
+ encryptedRefreshToken = data.refreshToken,
+ encryptedAccessToken = data.accessToken,
+ expiresAt = data.expiresAt.asInstantMilli(),
+ )
+ )
+
+ data class Secrets(
+ val credentialId: Int,
+ val privateKey: String,
+ val encryptedRefreshToken: String,
+ var encryptedAccessToken: String,
+ var expiresAt: Instant,
+ )
+}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/Credential.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/Credential.kt
new file mode 100644
index 000000000..272aa5244
--- /dev/null
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/Credential.kt
@@ -0,0 +1,19 @@
+package org.dreamexposure.discal.core.`object`.new
+
+import org.dreamexposure.discal.core.database.CredentialData
+import org.dreamexposure.discal.core.extensions.asInstantMilli
+import java.time.Instant
+
+data class Credential(
+ val credentialNumber: Int,
+ var encryptedRefreshToken: String,
+ var encryptedAccessToken: String,
+ var expiresAt: Instant,
+) {
+ constructor(data: CredentialData) : this(
+ credentialNumber = data.credentialNumber,
+ encryptedRefreshToken = data.refreshToken,
+ encryptedAccessToken = data.accessToken,
+ expiresAt = data.expiresAt.asInstantMilli(),
+ )
+}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/object/rest/HeartbeatRequest.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/object/rest/HeartbeatRequest.kt
index 30a1faaf8..fa94e96ab 100644
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/object/rest/HeartbeatRequest.kt
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/object/rest/HeartbeatRequest.kt
@@ -1,5 +1,6 @@
package org.dreamexposure.discal.core.`object`.rest
+import com.fasterxml.jackson.annotation.JsonProperty
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.dreamexposure.discal.core.`object`.network.discal.BotInstanceData
@@ -9,9 +10,11 @@ import org.dreamexposure.discal.core.`object`.network.discal.InstanceData
data class HeartbeatRequest(
val type: HeartbeatType,
+ @JsonProperty("instance")
@SerialName("instance")
val instanceData: InstanceData? = null,
+ @JsonProperty("bot_instance")
@SerialName("bot_instance")
val botInstanceData: BotInstanceData? = null
)
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/object/web/WebGuild.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/object/web/WebGuild.kt
index 224cb43f5..5ccf9447a 100644
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/object/web/WebGuild.kt
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/object/web/WebGuild.kt
@@ -14,12 +14,11 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.LongAsStringSerializer
import org.dreamexposure.discal.Application.Companion.getShardCount
-import org.dreamexposure.discal.core.`object`.BotSettings
-import org.dreamexposure.discal.core.`object`.GuildSettings
-import org.dreamexposure.discal.core.`object`.announcement.Announcement
import org.dreamexposure.discal.core.database.DatabaseManager
import org.dreamexposure.discal.core.exceptions.BotNotInGuildException
import org.dreamexposure.discal.core.extensions.discord4j.getMainCalendar
+import org.dreamexposure.discal.core.`object`.GuildSettings
+import org.dreamexposure.discal.core.`object`.announcement.Announcement
import reactor.core.publisher.Mono
import reactor.core.publisher.Mono.justOrEmpty
import reactor.function.TupleUtils
@@ -62,8 +61,7 @@ data class WebGuild(
val name = data.name()
val ico = data.icon().orElse("")
- val botNick = g.member(Snowflake.of(BotSettings.ID.get()))
- .data
+ val botNick = g.selfMember
.map(MemberData::nick)
.map { Possible.flatOpt(it) }
.flatMap { justOrEmpty(it) }
@@ -105,7 +103,7 @@ data class WebGuild(
val name = g.name
val icon = g.getIconUrl(Image.Format.PNG).orElse(null)
- val botNick = g.getMemberById(Snowflake.of(BotSettings.ID.get()))
+ val botNick = g.selfMember
.map(Member::getNickname)
.flatMap { justOrEmpty(it) }
.defaultIfEmpty("DisCal")
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/serializers/DurationAsStringSerializer.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/serializers/DurationAsStringSerializer.kt
index 5140e96ad..e17fee06f 100644
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/serializers/DurationAsStringSerializer.kt
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/serializers/DurationAsStringSerializer.kt
@@ -1,6 +1,5 @@
package org.dreamexposure.discal.core.serializers
-import discord4j.common.util.Snowflake
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
@@ -10,7 +9,7 @@ import kotlinx.serialization.encoding.Encoder
import java.time.Duration
object DurationAsStringSerializer : KSerializer {
- override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Duration", PrimitiveKind.STRING)
+ override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("DurationAsString", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Duration) = encoder.encodeString(value.toMillis().toString())
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/serializers/DurationMapper.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/serializers/DurationMapper.kt
new file mode 100644
index 000000000..1d25a957a
--- /dev/null
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/serializers/DurationMapper.kt
@@ -0,0 +1,30 @@
+package org.dreamexposure.discal.core.serializers
+
+import com.fasterxml.jackson.core.JsonGenerator
+import com.fasterxml.jackson.core.JsonParser
+import com.fasterxml.jackson.databind.DeserializationContext
+import com.fasterxml.jackson.databind.SerializerProvider
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer
+import com.fasterxml.jackson.databind.module.SimpleModule
+import com.fasterxml.jackson.databind.ser.std.StdSerializer
+import java.time.Duration
+
+class DurationMapper: SimpleModule() {
+ init {
+ addSerializer(DurationSerializer())
+ addDeserializer(Duration::class.java, DurationDeserializer())
+ }
+
+ class DurationSerializer : StdSerializer(Duration::class.java) {
+ override fun serialize(value: Duration?, gen: JsonGenerator?, provider: SerializerProvider?) {
+ gen?.writeString(value?.toMillis().toString())
+ }
+ }
+
+ class DurationDeserializer: StdDeserializer(Duration::class.java) {
+ override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): Duration {
+ val raw = p?.valueAsString
+ return if (raw != null) Duration.ofMillis(raw.toLong()) else throw IllegalStateException()
+ }
+ }
+}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/serializers/InstantAsStringSerializer.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/serializers/InstantAsStringSerializer.kt
index a9387bb1e..f2264b21c 100644
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/serializers/InstantAsStringSerializer.kt
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/serializers/InstantAsStringSerializer.kt
@@ -9,7 +9,7 @@ import kotlinx.serialization.encoding.Encoder
import java.time.Instant
object InstantAsStringSerializer : KSerializer {
- override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Duration", PrimitiveKind.STRING)
+ override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("InstantAsString", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeString(value.toString())
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/serializers/SnowflakeMapper.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/serializers/SnowflakeMapper.kt
new file mode 100644
index 000000000..97d64e0a1
--- /dev/null
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/serializers/SnowflakeMapper.kt
@@ -0,0 +1,30 @@
+package org.dreamexposure.discal.core.serializers
+
+import com.fasterxml.jackson.core.JsonGenerator
+import com.fasterxml.jackson.core.JsonParser
+import com.fasterxml.jackson.databind.DeserializationContext
+import com.fasterxml.jackson.databind.SerializerProvider
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer
+import com.fasterxml.jackson.databind.module.SimpleModule
+import com.fasterxml.jackson.databind.ser.std.StdSerializer
+import discord4j.common.util.Snowflake
+
+class SnowflakeMapper: SimpleModule() {
+ init {
+ addSerializer(SnowflakeSerializer())
+ addDeserializer(Snowflake::class.java, SnowflakeDeserializer())
+ }
+
+ class SnowflakeSerializer : StdSerializer(Snowflake::class.java) {
+ override fun serialize(value: Snowflake?, gen: JsonGenerator?, provider: SerializerProvider?) {
+ gen?.writeString(value?.asString())
+ }
+ }
+
+ class SnowflakeDeserializer: StdDeserializer(Snowflake::class.java) {
+ override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): Snowflake {
+ val raw = p?.valueAsString
+ return if (raw != null) Snowflake.of(raw) else throw IllegalStateException()
+ }
+ }
+}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/spring/BeanConfig.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/spring/BeanConfig.kt
deleted file mode 100644
index 7143cd059..000000000
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/spring/BeanConfig.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package org.dreamexposure.discal.core.spring
-
-import org.springframework.context.annotation.Bean
-import org.springframework.context.annotation.Configuration
-import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping
-
-@Configuration
-class BeanConfig {
-
- @Bean
- fun handlerMapping(): RequestMappingHandlerMapping {
- return RequestMappingHandlerMapping()
- }
-}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/spring/GlobalErrorHandler.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/spring/GlobalErrorHandler.kt
index b41f88f57..22de038ee 100644
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/spring/GlobalErrorHandler.kt
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/spring/GlobalErrorHandler.kt
@@ -2,12 +2,12 @@ package org.dreamexposure.discal.core.spring
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
-import org.dreamexposure.discal.core.`object`.rest.RestError
import org.dreamexposure.discal.core.exceptions.AccessRevokedException
import org.dreamexposure.discal.core.exceptions.AuthenticationException
import org.dreamexposure.discal.core.exceptions.NotFoundException
import org.dreamexposure.discal.core.exceptions.TeaPotException
import org.dreamexposure.discal.core.logger.LOGGER
+import org.dreamexposure.discal.core.`object`.rest.RestError
import org.dreamexposure.discal.core.utils.GlobalVal
import org.springframework.beans.TypeMismatchException
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler
@@ -27,7 +27,7 @@ class GlobalErrorHandler : ErrorWebExceptionHandler {
//Handle exceptions we have codes for
val restError: RestError = when (throwable) {
is ResponseStatusException -> {
- when (throwable.status) {
+ when (throwable.statusCode) {
HttpStatus.NOT_FOUND -> {
exchange.response.statusCode = HttpStatus.NOT_FOUND
LOGGER.trace("404 exchange | Path: ${exchange.request.path}")
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/spring/SecurityWebFilter.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/spring/SecurityWebFilter.kt
index f7d11ee3d..e550a5a4b 100644
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/spring/SecurityWebFilter.kt
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/spring/SecurityWebFilter.kt
@@ -1,8 +1,10 @@
package org.dreamexposure.discal.core.spring
-import org.dreamexposure.discal.core.`object`.BotSettings
+import kotlinx.coroutines.reactor.mono
import org.dreamexposure.discal.core.annotations.Authentication
-import org.dreamexposure.discal.core.database.DatabaseManager
+import org.dreamexposure.discal.core.business.ApiKeyService
+import org.dreamexposure.discal.core.business.SessionService
+import org.dreamexposure.discal.core.config.Config
import org.dreamexposure.discal.core.exceptions.AuthenticationException
import org.dreamexposure.discal.core.exceptions.EmptyNotAllowedException
import org.dreamexposure.discal.core.exceptions.TeaPotException
@@ -22,7 +24,11 @@ import java.util.concurrent.ConcurrentMap
@Component
@ConditionalOnProperty(name = ["discal.security.enabled"], havingValue = "true")
-class SecurityWebFilter(val handlerMapping: RequestMappingHandlerMapping) : WebFilter {
+class SecurityWebFilter(
+ private val sessionService: SessionService,
+ private val handlerMapping: RequestMappingHandlerMapping,
+ private val apiKeyService: ApiKeyService,
+) : WebFilter {
private val readOnlyKeys: ConcurrentMap = ConcurrentHashMap()
init {
@@ -78,14 +84,16 @@ class SecurityWebFilter(val handlerMapping: RequestMappingHandlerMapping) : WebF
authHeader.equals("teapot", true) -> {
Mono.error(TeaPotException())
}
- authHeader.equals(BotSettings.BOT_API_TOKEN.get()) -> { // This is from within discal network
+
+ authHeader.equals(Config.SECRET_DISCAL_API_KEY.getString()) -> { // This is from within discal network
Mono.just(Authentication.AccessLevel.ADMIN)
}
+
readOnlyKeys.containsKey(authHeader) -> { // Read-only key granted for embed pages
Mono.just(Authentication.AccessLevel.READ)
}
authHeader.startsWith("Bearer ") -> {
- DatabaseManager.getSessionData(authHeader.substringAfter("Bearer ")).flatMap { session ->
+ mono { sessionService.getSession(authHeader.substringAfter("Bearer ")) }.flatMap { session ->
if (session.expiresAt.isAfter(Instant.now())) {
Mono.just(Authentication.AccessLevel.WRITE)
} else {
@@ -95,8 +103,8 @@ class SecurityWebFilter(val handlerMapping: RequestMappingHandlerMapping) : WebF
}
else -> {
// Check if this is an API key
- DatabaseManager.getAPIAccount(authHeader).flatMap { acc ->
- if (!acc.blocked) {
+ mono { apiKeyService.getKey(authHeader) }.flatMap { key ->
+ if (!key.blocked) {
Mono.just(Authentication.AccessLevel.WRITE)
} else {
Mono.error(AuthenticationException("API key blocked"))
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/utils/GlobalVal.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/utils/GlobalVal.kt
index 2031ded72..b1fe0629a 100644
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/utils/GlobalVal.kt
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/utils/GlobalVal.kt
@@ -8,26 +8,23 @@ import io.github.furstenheim.OptionsBuilder
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
-import org.jsoup.safety.Whitelist
+import org.jsoup.safety.Safelist
import org.slf4j.Marker
import org.slf4j.MarkerFactory
object GlobalVal {
- @JvmStatic
var iconUrl: String = ""
- @JvmStatic
- @Deprecated(message = "Using linebreak char should be sufficient")
- val lineBreak: String = System.getProperty("line.separator")
- @JvmStatic
val devUserIds = listOf(
Snowflake.of(130510525770629121L), //NovaFox161
Snowflake.of(135995653095555073L), //Dannie <3
)
- @JvmStatic
val discalColor: Color = Color.of(56, 138, 237)
+ val errorColor: Color = Color.of(248, 38, 48)
+ val warnColor: Color = Color.of(232, 150, 0)
+
const val discordApiUrl = "https://discord.com/api/v9"
@@ -42,9 +39,9 @@ object GlobalVal {
ignoreUnknownKeys = true
}
- val HTML_WHITELIST: Whitelist
+ val HTML_WHITELIST: Safelist
get() {
- return Whitelist.basic()
+ return Safelist.basic()
.preserveRelativeLinks(false)
.removeAttributes("sub", "sup", "small")
}
@@ -61,7 +58,6 @@ object GlobalVal {
)
}
- @JvmStatic
val DEFAULT: Marker = MarkerFactory.getMarker("DISCAL_WEBHOOK_DEFAULT")
val STATUS: Marker = MarkerFactory.getMarker("DISCAL_WEBHOOK_STATUS")
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/utils/JsonUtil.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/utils/JsonUtil.kt
deleted file mode 100644
index ec2522c49..000000000
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/utils/JsonUtil.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package org.dreamexposure.discal.core.utils
-
-import kotlinx.serialization.InternalSerializationApi
-import kotlinx.serialization.KSerializer
-import kotlinx.serialization.json.Json
-import kotlinx.serialization.serializer
-import org.json.JSONObject
-import kotlin.jvm.internal.Reflection
-
-@OptIn(InternalSerializationApi::class)
-@Deprecated("Phase out org.json usage")
-object JsonUtil {
- fun encodeToJSON(clazz: Class, data: T): JSONObject {
- val kClass = Reflection.createKotlinClass(clazz)
- val serializer = kClass.serializer() as KSerializer
-
- return JSONObject(GlobalVal.JSON_FORMAT.encodeToString(serializer, data))
- }
-
- fun encodeToString(clazz: Class, data: T): String = encodeToJSON(clazz, data).toString()
-
- fun decodeFromJSON(clazz: Class, json: JSONObject): T = decodeFromString(clazz, json.toString())
-
- fun decodeFromString(clazz: Class, str: String): T {
- val kClass = Reflection.createKotlinClass(clazz)
- val serializer = kClass.serializer()
-
- return GlobalVal.JSON_FORMAT.decodeFromString(serializer, str) as T
- }
-}
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/wrapper/google/GoogleAuthWrapper.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/wrapper/google/GoogleAuthWrapper.kt
index dabb8134d..0879dfd20 100644
--- a/core/src/main/kotlin/org/dreamexposure/discal/core/wrapper/google/GoogleAuthWrapper.kt
+++ b/core/src/main/kotlin/org/dreamexposure/discal/core/wrapper/google/GoogleAuthWrapper.kt
@@ -11,17 +11,16 @@ import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Request
import okhttp3.Response
-import org.dreamexposure.discal.core.`object`.BotSettings
-import org.dreamexposure.discal.core.`object`.calendar.CalendarData
-import org.dreamexposure.discal.core.`object`.google.ClientData
-import org.dreamexposure.discal.core.`object`.google.GoogleAuthPoll
-import org.dreamexposure.discal.core.`object`.network.discal.CredentialData
-import org.dreamexposure.discal.core.`object`.rest.RestError
+import org.dreamexposure.discal.core.config.Config
import org.dreamexposure.discal.core.database.DatabaseManager
import org.dreamexposure.discal.core.enums.calendar.CalendarHost
import org.dreamexposure.discal.core.exceptions.EmptyNotAllowedException
import org.dreamexposure.discal.core.exceptions.google.GoogleAuthCancelException
import org.dreamexposure.discal.core.logger.LOGGER
+import org.dreamexposure.discal.core.`object`.calendar.CalendarData
+import org.dreamexposure.discal.core.`object`.google.GoogleAuthPoll
+import org.dreamexposure.discal.core.`object`.network.discal.CredentialData
+import org.dreamexposure.discal.core.`object`.rest.RestError
import org.dreamexposure.discal.core.utils.GlobalVal.DEFAULT
import org.dreamexposure.discal.core.utils.GlobalVal.HTTP_CLIENT
import org.dreamexposure.discal.core.utils.GlobalVal.JSON_FORMAT
@@ -34,8 +33,6 @@ import com.google.api.services.calendar.Calendar as GoogleCalendarService
@Suppress("BlockingMethodInNonBlockingContext")
object GoogleAuthWrapper {
- private val clientData = ClientData(BotSettings.GOOGLE_CLIENT_ID.get(), BotSettings.GOOGLE_CLIENT_SECRET.get())
-
private val discalTokens: MutableMap = ConcurrentHashMap()
private val externalTokens: MutableMap = ConcurrentHashMap()
@@ -68,15 +65,15 @@ object GoogleAuthWrapper {
}
return Mono.fromCallable {
- val url = "${BotSettings.CAM_URL.get()}/v1/token".toHttpUrlOrNull()!!.newBuilder()
- .addQueryParameter("host", CalendarHost.GOOGLE.name)
- .addQueryParameter("id", credentialId.toString())
- .build()
+ val url = "${Config.URL_CAM.getString()}/v1/token".toHttpUrlOrNull()!!.newBuilder()
+ .addQueryParameter("host", CalendarHost.GOOGLE.name)
+ .addQueryParameter("id", credentialId.toString())
+ .build()
val request = Request.Builder().get()
- .header("Authorization", BotSettings.BOT_API_TOKEN.get())
- .url(url)
- .build()
+ .header("Authorization", Config.SECRET_DISCAL_API_KEY.getString())
+ .url(url)
+ .build()
HTTP_CLIENT.newCall(request).execute()
}.subscribeOn(Schedulers.boundedElastic()).flatMap { response ->
@@ -109,16 +106,16 @@ object GoogleAuthWrapper {
}
return Mono.fromCallable {
- val url = "${BotSettings.CAM_URL.get()}/v1/token".toHttpUrlOrNull()!!.newBuilder()
- .addQueryParameter("host", calData.host.name)
- .addQueryParameter("guild", calData.guildId.asString())
- .addQueryParameter("id", calData.calendarNumber.toString())
- .build()
+ val url = "${Config.URL_CAM.getString()}/v1/token".toHttpUrlOrNull()!!.newBuilder()
+ .addQueryParameter("host", calData.host.name)
+ .addQueryParameter("guild", calData.guildId.asString())
+ .addQueryParameter("id", calData.calendarNumber.toString())
+ .build()
val request = Request.Builder().get()
- .header("Authorization", BotSettings.BOT_API_TOKEN.get())
- .url(url)
- .build()
+ .header("Authorization", Config.SECRET_DISCAL_API_KEY.getString())
+ .url(url)
+ .build()
HTTP_CLIENT.newCall(request).execute()
}.subscribeOn(Schedulers.boundedElastic()).flatMap { response ->
@@ -168,12 +165,12 @@ object GoogleAuthWrapper {
.switchIfEmpty(Mono.error(EmptyNotAllowedException()))
}
- fun randomCredentialId() = Random.nextInt(BotSettings.CREDENTIALS_COUNT.get().toInt())
+ fun randomCredentialId() = Random.nextInt(Config.SECRET_GOOGLE_CREDENTIAL_COUNT.getInt())
fun requestDeviceCode(): Mono {
return Mono.fromCallable {
val body = FormBody.Builder()
- .addEncoded("client_id", clientData.clientId)
+ .addEncoded("client_id", Config.SECRET_GOOGLE_CLIENT_ID.getString())
.addEncoded("scope", CalendarScopes.CALENDAR)
.build()
@@ -190,8 +187,8 @@ object GoogleAuthWrapper {
fun requestPollResponse(poll: GoogleAuthPoll): Mono {
return Mono.fromCallable {
val body = FormBody.Builder()
- .addEncoded("client_id", clientData.clientId)
- .addEncoded("client_secret", clientData.clientSecret)
+ .addEncoded("client_id", Config.SECRET_GOOGLE_CLIENT_ID.getString())
+ .addEncoded("client_secret", Config.SECRET_GOOGLE_CLIENT_SECRET.getString())
.addEncoded("code", poll.deviceCode)
.addEncoded("grant_type", "http://oauth.net/grant_type/device/1.0")
.build()
diff --git a/core/src/main/kotlin/org/dreamexposure/discal/typealiases.kt b/core/src/main/kotlin/org/dreamexposure/discal/typealiases.kt
new file mode 100644
index 000000000..d893a973c
--- /dev/null
+++ b/core/src/main/kotlin/org/dreamexposure/discal/typealiases.kt
@@ -0,0 +1,11 @@
+package org.dreamexposure.discal
+
+import org.dreamexposure.discal.core.cache.CacheRepository
+import org.dreamexposure.discal.core.`object`.new.Calendar
+import org.dreamexposure.discal.core.`object`.new.Credential
+
+// Cache
+//typealias GuildSettingsCache = CacheRepository
+typealias CredentialsCache = CacheRepository
+typealias OauthStateCache = CacheRepository
+typealias CalendarCache = CacheRepository>
diff --git a/core/src/main/resources/commands/dev.json b/core/src/main/resources/commands/dev/dev.json
similarity index 100%
rename from core/src/main/resources/commands/dev.json
rename to core/src/main/resources/commands/dev/dev.json
diff --git a/core/src/main/resources/commands/announcement.json b/core/src/main/resources/commands/global/announcement.json
similarity index 100%
rename from core/src/main/resources/commands/announcement.json
rename to core/src/main/resources/commands/global/announcement.json
diff --git a/core/src/main/resources/commands/calendar.json b/core/src/main/resources/commands/global/calendar.json
similarity index 100%
rename from core/src/main/resources/commands/calendar.json
rename to core/src/main/resources/commands/global/calendar.json
diff --git a/core/src/main/resources/commands/discal.json b/core/src/main/resources/commands/global/discal.json
similarity index 100%
rename from core/src/main/resources/commands/discal.json
rename to core/src/main/resources/commands/global/discal.json
diff --git a/core/src/main/resources/commands/displayCal.json b/core/src/main/resources/commands/global/displayCal.json
similarity index 100%
rename from core/src/main/resources/commands/displayCal.json
rename to core/src/main/resources/commands/global/displayCal.json
diff --git a/core/src/main/resources/commands/event.json b/core/src/main/resources/commands/global/event.json
similarity index 97%
rename from core/src/main/resources/commands/event.json
rename to core/src/main/resources/commands/global/event.json
index 618ec133f..ca74f5c58 100644
--- a/core/src/main/resources/commands/event.json
+++ b/core/src/main/resources/commands/global/event.json
@@ -250,6 +250,12 @@
"required": false,
"min_value": 0,
"max_value": 59
+ },
+ {
+ "name": "keep-duration",
+ "type": 5,
+ "description": "Adjusts the end time to keep the same duration (overrides guild-level setting)",
+ "required": false
}
]
},
@@ -440,6 +446,12 @@
"required": false,
"min_value": 0,
"max_value": 59
+ },
+ {
+ "name": "keep-duration",
+ "type": 5,
+ "description": "Adjusts the start time to keep the same duration (overrides guild-level setting)",
+ "required": false
}
]
},
diff --git a/core/src/main/resources/commands/events.json b/core/src/main/resources/commands/global/events.json
similarity index 100%
rename from core/src/main/resources/commands/events.json
rename to core/src/main/resources/commands/global/events.json
diff --git a/core/src/main/resources/commands/help.json b/core/src/main/resources/commands/global/help.json
similarity index 100%
rename from core/src/main/resources/commands/help.json
rename to core/src/main/resources/commands/global/help.json
diff --git a/core/src/main/resources/commands/linkCal.json b/core/src/main/resources/commands/global/linkCal.json
similarity index 100%
rename from core/src/main/resources/commands/linkCal.json
rename to core/src/main/resources/commands/global/linkCal.json
diff --git a/core/src/main/resources/commands/rsvp.json b/core/src/main/resources/commands/global/rsvp.json
similarity index 100%
rename from core/src/main/resources/commands/rsvp.json
rename to core/src/main/resources/commands/global/rsvp.json
diff --git a/core/src/main/resources/commands/settings.json b/core/src/main/resources/commands/global/settings.json
similarity index 87%
rename from core/src/main/resources/commands/settings.json
rename to core/src/main/resources/commands/global/settings.json
index e8563da46..24427472f 100644
--- a/core/src/main/resources/commands/settings.json
+++ b/core/src/main/resources/commands/global/settings.json
@@ -94,6 +94,19 @@
}
]
},
+ {
+ "name": "keep-event-duration",
+ "type": 1,
+ "description": "Toggles whether to keep an event's duration when changing the start or end time (default false)",
+ "options": [
+ {
+ "name": "value",
+ "type": 5,
+ "description": "Whether to keep an event's duration",
+ "required": true
+ }
+ ]
+ },
{
"name": "branding",
"type": 1,
diff --git a/core/src/main/resources/commands/time.json b/core/src/main/resources/commands/global/time.json
similarity index 100%
rename from core/src/main/resources/commands/time.json
rename to core/src/main/resources/commands/global/time.json
diff --git a/core/src/main/resources/commands/addcal.json b/core/src/main/resources/commands/premium/addcal.json
similarity index 100%
rename from core/src/main/resources/commands/addcal.json
rename to core/src/main/resources/commands/premium/addcal.json
diff --git a/core/src/main/resources/db/migration/V26__GuildSettings_KeepDuration.sql b/core/src/main/resources/db/migration/V26__GuildSettings_KeepDuration.sql
new file mode 100644
index 000000000..9455d55a5
--- /dev/null
+++ b/core/src/main/resources/db/migration/V26__GuildSettings_KeepDuration.sql
@@ -0,0 +1,3 @@
+ALTER TABLE guild_settings
+ ADD COLUMN event_keep_duration BIT NOT NULL DEFAULT 0
+ AFTER announcement_style;
diff --git a/core/src/main/resources/i18n/command/settings/settings.properties b/core/src/main/resources/i18n/command/settings/settings.properties
index 0291c4394..4b10aec39 100644
--- a/core/src/main/resources/i18n/command/settings/settings.properties
+++ b/core/src/main/resources/i18n/command/settings/settings.properties
@@ -12,3 +12,7 @@ style.success=Announcement Style successfully changed to `{0}`.
lang.success=Successfully changed default language!
format.success=Time Format successfully changed to `{0}`.
brand.success=Server branding has been set to `{0}`.
+eventKeepDuration.success.true=Events will keep their duration by default when adjusting the start or end of the event.\n\
+ This can be overridden when changing an event's start/end.
+eventKeepDuration.success.false=Events will *not* keep their duration by default when adjusting the start or end of the event.\n\
+ This can be overridden when changing an event's start/end.
diff --git a/core/src/main/resources/i18n/embed/settings.properties b/core/src/main/resources/i18n/embed/settings.properties
index 750c7012f..05b3cea88 100644
--- a/core/src/main/resources/i18n/embed/settings.properties
+++ b/core/src/main/resources/i18n/embed/settings.properties
@@ -2,6 +2,7 @@ view.title=DisCal Guild Settings
view.field.role=Control Role
view.field.style=Announcement Style
view.field.format=Time Format
+view.field.eventKeepDuration=Keep Event Duration
view.field.lang=Language
view.field.patron=Patron Guild
view.field.dev=Dev Guild
diff --git a/core/src/main/resources/logback.xml b/core/src/main/resources/logback.xml
index 2c88ecf0f..965db32cb 100644
--- a/core/src/main/resources/logback.xml
+++ b/core/src/main/resources/logback.xml
@@ -16,6 +16,8 @@
+
+
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 000000000..bc746853f
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,78 @@
+version: "3"
+
+services:
+ mysql:
+ image: mysql:8.0
+ # NOTE: use of "mysql_native_password" is not recommended: https://dev.mysql.com/doc/refman/8.0/en/upgrading-from-previous-series.html#upgrade-caching-sha2-password
+ # This is only for local debugging and development. DO NOT USE IN PRODUCTION!!!!!
+ command: --default-authentication-plugin=mysql_native_password
+ environment:
+ - MYSQL_ROOT_PASSWORD=password
+ - MYSQL_USER=discal
+ - MYSQL_PASSWORD=password
+ - MYSQL_DATABASE=discal
+ restart: unless-stopped
+
+ redis:
+ image: redis:alpine
+ restart: unless-stopped
+
+ api:
+ image: rg.nl-ams.scw.cloud/dreamexposure/discal-server:latest
+ environment:
+ - JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
+ ports:
+ - "8080:8080"
+ - "5005:5005"
+ volumes:
+ - ./.docker/api:/discal
+ working_dir: /discal
+ depends_on:
+ - mysql
+ - redis
+
+ cam:
+ image: rg.nl-ams.scw.cloud/dreamexposure/discal-cam:latest
+ environment:
+ - JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
+ ports:
+ - "8081:8080"
+ - "5006:5005"
+ volumes:
+ - ./.docker/cam:/discal
+ working_dir: /discal
+ depends_on:
+ - mysql
+ - redis
+ - api
+
+ bot:
+ image: rg.nl-ams.scw.cloud/dreamexposure/discal-client:latest
+ environment:
+ - JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
+ ports:
+ - "5007:5005"
+ volumes:
+ - ./.docker/bot:/discal
+ working_dir: /discal
+ depends_on:
+ - mysql
+ - redis
+ - api
+ - cam
+
+ web:
+ image: rg.nl-ams.scw.cloud/dreamexposure/discal-web:latest
+ environment:
+ - JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
+ ports:
+ - "3000:8080"
+ - "5008:5005"
+ volumes:
+ - ./.docker/web:/discal
+ working_dir: /discal
+ depends_on:
+ - api
+
+volumes:
+ discal_data: {}
diff --git a/gradle.properties b/gradle.properties
index 41cf63f9a..5fb9c7a94 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,38 +1,50 @@
-kotlinVersion=1.6.10
-kotlinxSerializationVersion=1.3.2
+# TODO: I eventually want to get spring's dependency management (BOM) plugin working one day, but its not resolving versions somehow?
-gitPropsVersion=2.3.2
-jibVersion=3.1.4
+# Language
+kotlinVersion=1.9.0
-discord4jVersion=3.2.2
-discord4jStoresVersion=3.2.1
+# Plugins
+springDependencyManagementVersion=1.1.3
+jibVersion=3.3.2
+gitPropertiesVersion=2.4.1
-thymeleafVersion=3.0.14.RELEASE
-thymeleafSecurityVersion=3.0.4.RELEASE
-thymeleafLayoutVersion=3.0.0
+# Buildscript tooling
+kotlinPoetVersion=1.14.2
-springVersion=2.5.6
-springSecurityVersion=5.5.1
-springSessionVersion=2.5.3
-springR2Version=5.3.13
+# Tools
+kotlinxCoroutinesReactorVersion=1.7.3
+reactorKotlinExtensions=1.2.2
-googleCoreVersion=1.32.1
-googleCalendarVersion=v3-rev20211026-1.32.1
+# Discord
+discord4jVersion=3.2.5
+discord4jStoresVersion=3.2.2
+discordWebhookVersion=0.8.4
-r2MysqlVersion=0.8.2.RELEASE
-r2PoolVersion=0.9.0.RELEASE
+# Spring
+springVersion=3.1.2
-nettyVersion=4.1.56.Final
-reactorBomVersion=2020.0.16
+# Thymeleaf
+thymeleafVersion=3.1.2.RELEASE
-slfVersion=1.7.31
-jsonVersion=20211205
-okHttpVersion=4.9.3
-discordWebhookVersion=0.7.5
-copyDownVersion=1.0
-flywayVersion=8.0.2
-mysqlConnectorVersion=8.0.25
-hikariVersion=5.0.1
-jacksonKotlinModVersion=2.13.1
+# Database
+flywayVersion=9.3.0
+mikuR2dbcMySqlVersion=0.8.2.RELEASE
+mySqlConnectorJava=8.0.33
-baseImage=eclipse-temurin:16-jdk-alpine
+# Serialization
+kotlinxSerializationJsonVersion=1.5.1
+jacksonVersion=2.15.2
+jsonVersion=20230618
+
+# Google libs
+googleApiClientVersion=2.0.0
+googleServicesCalendarVersion=v3-rev20220715-2.0.0
+googleOauthClientVersion=1.34.1
+
+# Various Libs
+okhttpVersion=4.11.0
+copyDownVersion=1.1
+jsoupVersion=1.16.1
+
+# Jib properties
+baseImage=eclipse-temurin:17-jdk-alpine
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index e708b1c02..033e24c4c 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index ffed3a254..c747538fb 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip
+networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 4f906e0c8..fcb6fca14 100755
--- a/gradlew
+++ b/gradlew
@@ -1,7 +1,7 @@
-#!/usr/bin/env sh
+#!/bin/sh
#
-# Copyright 2015 the original author or authors.
+# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -17,67 +17,98 @@
#
##############################################################################
-##
-## Gradle start up script for UN*X
-##
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
##############################################################################
# Attempt to set APP_HOME
+
# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
+MAX_FD=maximum
warn () {
echo "$*"
-}
+} >&2
die () {
echo
echo "$*"
echo
exit 1
-}
+} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MINGW* )
- msys=true
- ;;
- NONSTOP* )
- nonstop=true
- ;;
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@@ -87,9 +118,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACMD=$JAVA_HOME/jre/sh/java
else
- JAVACMD="$JAVA_HOME/bin/java"
+ JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -98,88 +129,120 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
- JAVACMD="java"
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
+ fi
fi
# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
fi
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
- GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
-if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
- APP_HOME=`cygpath --path --mixed "$APP_HOME"`
- CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-
- JAVACMD=`cygpath --unix "$JAVACMD"`
-
- # We build the pattern for arguments to be converted via cygpath
- ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
- SEP=""
- for dir in $ROOTDIRSRAW ; do
- ROOTDIRS="$ROOTDIRS$SEP$dir"
- SEP="|"
- done
- OURCYGPATTERN="(^($ROOTDIRS))"
- # Add a user-defined pattern to the cygpath arguments
- if [ "$GRADLE_CYGPATTERN" != "" ] ; then
- OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
- fi
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
# Now convert the arguments - kludge to limit ourselves to /bin/sh
- i=0
- for arg in "$@" ; do
- CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
- CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
-
- if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
- eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
- else
- eval `echo args$i`="\"$arg\""
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
fi
- i=`expr $i + 1`
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
done
- case $i in
- 0) set -- ;;
- 1) set -- "$args0" ;;
- 2) set -- "$args0" "$args1" ;;
- 3) set -- "$args0" "$args1" "$args2" ;;
- 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
- esac
fi
-# Escape application args
-save () {
- for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
- echo " "
-}
-APP_ARGS=`save "$@"`
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
index 107acd32c..93e3f59f1 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
-@if "%DEBUG%" == "" @echo off
+@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,7 +25,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto execute
+if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
+if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
diff --git a/server/build.gradle.kts b/server/build.gradle.kts
index a598d85a2..913db524c 100644
--- a/server/build.gradle.kts
+++ b/server/build.gradle.kts
@@ -1,32 +1,26 @@
plugins {
+ // Kotlin
+ id("org.jetbrains.kotlin.plugin.allopen")
+
+ // Spring
kotlin("plugin.spring")
id("org.springframework.boot")
- id("org.jetbrains.kotlin.plugin.allopen")
+ id("io.spring.dependency-management")
+
+ // Tooling
id("com.google.cloud.tools.jib")
}
-val springVersion: String by properties
-val springSessionVersion: String by properties
-val springR2Version: String by properties
+// Versions --- found in gradle.properties
+// Database
val flywayVersion: String by properties
-val mysqlConnectorVersion: String by properties
-val hikariVersion: String by properties
-val jacksonKotlinModVersion: String by properties
dependencies {
api(project(":core"))
- //Database stuff
+ // Database
implementation("org.flywaydb:flyway-core:$flywayVersion")
- implementation("mysql:mysql-connector-java:$mysqlConnectorVersion")
- implementation("com.zaxxer:HikariCP:$hikariVersion")
-
- //Spring libs
- implementation("org.springframework.session:spring-session-data-redis:$springSessionVersion")
- implementation("org.springframework:spring-r2dbc:$springR2Version")
-
- //jackson for kotlin
- implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonKotlinModVersion")
+ implementation("org.flywaydb:flyway-mysql:$flywayVersion")
}
kotlin {
@@ -38,14 +32,13 @@ kotlin {
}
jib {
- var imageVersion = version.toString()
- if (imageVersion.contains("SNAPSHOT")) imageVersion = "latest"
+ to {
+ image = "rg.nl-ams.scw.cloud/dreamexposure/discal-server"
+ tags = mutableSetOf("latest", version.toString())
+ }
- to.image = "rg.nl-ams.scw.cloud/dreamexposure/discal-server:$imageVersion"
val baseImage: String by properties
from.image = baseImage
-
- container.creationTime = "USE_CURRENT_TIMESTAMP"
}
tasks {
diff --git a/server/src/main/kotlin/org/dreamexposure/discal/server/DisCalServer.kt b/server/src/main/kotlin/org/dreamexposure/discal/server/DisCalServer.kt
index f87768574..1f794f39a 100644
--- a/server/src/main/kotlin/org/dreamexposure/discal/server/DisCalServer.kt
+++ b/server/src/main/kotlin/org/dreamexposure/discal/server/DisCalServer.kt
@@ -1,16 +1,14 @@
package org.dreamexposure.discal.server
+import jakarta.annotation.PreDestroy
import org.dreamexposure.discal.Application
-import org.dreamexposure.discal.core.`object`.BotSettings
+import org.dreamexposure.discal.core.config.Config
import org.dreamexposure.discal.core.database.DatabaseManager
import org.dreamexposure.discal.core.logger.LOGGER
import org.dreamexposure.discal.core.utils.GlobalVal.DEFAULT
import org.dreamexposure.discal.core.utils.GlobalVal.STATUS
import org.springframework.boot.builder.SpringApplicationBuilder
import org.springframework.stereotype.Component
-import java.io.FileReader
-import java.util.*
-import javax.annotation.PreDestroy
import kotlin.system.exitProcess
@@ -26,15 +24,11 @@ class DisCalServer {
companion object {
@JvmStatic
fun main(args: Array) {
- //Get settings
- val p = Properties()
- p.load(FileReader("settings.properties"))
- BotSettings.init(p)
+ Config.init()
//Start up spring
try {
SpringApplicationBuilder(Application::class.java)
- .profiles(BotSettings.PROFILE.get())
.build()
.run(*args)
LOGGER.info(STATUS, "API is now online")
diff --git a/server/src/main/kotlin/org/dreamexposure/discal/server/conf/WebFluxConfig.kt b/server/src/main/kotlin/org/dreamexposure/discal/server/conf/WebFluxConfig.kt
deleted file mode 100644
index a831a5c10..000000000
--- a/server/src/main/kotlin/org/dreamexposure/discal/server/conf/WebFluxConfig.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-package org.dreamexposure.discal.server.conf
-
-import io.r2dbc.spi.ConnectionFactories
-import io.r2dbc.spi.ConnectionFactory
-import io.r2dbc.spi.ConnectionFactoryOptions
-import org.dreamexposure.discal.core.`object`.BotSettings
-import org.dreamexposure.discal.core.utils.GlobalVal
-import org.springframework.boot.web.server.ConfigurableWebServerFactory
-import org.springframework.boot.web.server.ErrorPage
-import org.springframework.boot.web.server.WebServerFactoryCustomizer
-import org.springframework.context.annotation.Bean
-import org.springframework.context.annotation.Configuration
-import org.springframework.data.redis.connection.RedisPassword
-import org.springframework.data.redis.connection.RedisStandaloneConfiguration
-import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
-import org.springframework.http.HttpStatus
-import org.springframework.http.codec.ServerCodecConfigurer
-import org.springframework.http.codec.json.KotlinSerializationJsonDecoder
-import org.springframework.http.codec.json.KotlinSerializationJsonEncoder
-import org.springframework.web.reactive.config.CorsRegistry
-import org.springframework.web.reactive.config.EnableWebFlux
-import org.springframework.web.reactive.config.WebFluxConfigurer
-
-@Configuration
-@EnableWebFlux
-class WebFluxConfig : WebServerFactoryCustomizer, WebFluxConfigurer {
-
- override fun customize(factory: ConfigurableWebServerFactory?) {
- factory?.addErrorPages(ErrorPage(HttpStatus.NOT_FOUND, "/"))
- }
-
- override fun addCorsMappings(registry: CorsRegistry) {
- registry.addMapping("/**")
- .allowedOrigins("*")
- .allowedMethods("GET", "POST", "OPTIONS")
- .allowedHeaders("Authorization", "Content-Type")
- .maxAge(600)
- }
-
-
- @Bean(name = ["redisDatasource"])
- fun redisConnectionFactory(): LettuceConnectionFactory {
- val rsc = RedisStandaloneConfiguration()
- rsc.hostName = BotSettings.REDIS_HOSTNAME.get()
- rsc.port = BotSettings.REDIS_PORT.get().toInt()
- if (BotSettings.REDIS_USE_PASSWORD.get().equals("true", true))
- rsc.password = RedisPassword.of(BotSettings.REDIS_PASSWORD.get())
-
- return LettuceConnectionFactory(rsc)
- }
-
- @Bean(name = ["mysqlDatasource"])
- fun mysqlConnectionFactory(): ConnectionFactory {
- return ConnectionFactories.get(ConnectionFactoryOptions.builder()
- .option(ConnectionFactoryOptions.DRIVER, "pool")
- .option(ConnectionFactoryOptions.PROTOCOL, "mysql")
- .option(ConnectionFactoryOptions.HOST, BotSettings.SQL_HOST.get())
- .option(ConnectionFactoryOptions.PORT, BotSettings.SQL_PORT.get().toInt())
- .option(ConnectionFactoryOptions.USER, BotSettings.SQL_USER.get())
- .option(ConnectionFactoryOptions.PASSWORD, BotSettings.SQL_PASS.get())
- .option(ConnectionFactoryOptions.DATABASE, BotSettings.SQL_DB.get())
- .build())
- }
-
- override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
- val codecs = configurer.defaultCodecs()
- codecs.kotlinSerializationJsonDecoder(KotlinSerializationJsonDecoder(GlobalVal.JSON_FORMAT))
- codecs.kotlinSerializationJsonEncoder(KotlinSerializationJsonEncoder(GlobalVal.JSON_FORMAT))
- }
-}
diff --git a/server/src/main/kotlin/org/dreamexposure/discal/server/conf/DiscordConfiguration.kt b/server/src/main/kotlin/org/dreamexposure/discal/server/config/DiscordConfig.kt
similarity index 56%
rename from server/src/main/kotlin/org/dreamexposure/discal/server/conf/DiscordConfiguration.kt
rename to server/src/main/kotlin/org/dreamexposure/discal/server/config/DiscordConfig.kt
index 7eb7f3cba..5415d8586 100644
--- a/server/src/main/kotlin/org/dreamexposure/discal/server/conf/DiscordConfiguration.kt
+++ b/server/src/main/kotlin/org/dreamexposure/discal/server/config/DiscordConfig.kt
@@ -1,16 +1,15 @@
-package org.dreamexposure.discal.server.conf
+package org.dreamexposure.discal.server.config
import discord4j.core.DiscordClient
import discord4j.core.DiscordClientBuilder
-import org.dreamexposure.discal.core.`object`.BotSettings
+import org.dreamexposure.discal.core.config.Config
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
-class DiscordConfiguration {
-
+class DiscordConfig {
@Bean
fun discordClient(): DiscordClient {
- return DiscordClientBuilder.create(BotSettings.TOKEN.get()).build()
+ return DiscordClientBuilder.create(Config.SECRET_BOT_TOKEN.getString()).build()
}
}
diff --git a/server/src/main/kotlin/org/dreamexposure/discal/server/config/WebFluxConfig.kt b/server/src/main/kotlin/org/dreamexposure/discal/server/config/WebFluxConfig.kt
new file mode 100644
index 000000000..d035dd5ad
--- /dev/null
+++ b/server/src/main/kotlin/org/dreamexposure/discal/server/config/WebFluxConfig.kt
@@ -0,0 +1,37 @@
+package org.dreamexposure.discal.server.config
+
+import org.dreamexposure.discal.core.utils.GlobalVal
+import org.springframework.boot.web.server.ConfigurableWebServerFactory
+import org.springframework.boot.web.server.ErrorPage
+import org.springframework.boot.web.server.WebServerFactoryCustomizer
+import org.springframework.context.annotation.Configuration
+import org.springframework.http.HttpStatus
+import org.springframework.http.codec.ServerCodecConfigurer
+import org.springframework.http.codec.json.KotlinSerializationJsonDecoder
+import org.springframework.http.codec.json.KotlinSerializationJsonEncoder
+import org.springframework.web.reactive.config.CorsRegistry
+import org.springframework.web.reactive.config.EnableWebFlux
+import org.springframework.web.reactive.config.WebFluxConfigurer
+
+@Configuration
+@EnableWebFlux
+class WebFluxConfig : WebServerFactoryCustomizer, WebFluxConfigurer {
+
+ override fun customize(factory: ConfigurableWebServerFactory?) {
+ factory?.addErrorPages(ErrorPage(HttpStatus.NOT_FOUND, "/"))
+ }
+
+ override fun addCorsMappings(registry: CorsRegistry) {
+ registry.addMapping("/**")
+ .allowedOrigins("*")
+ .allowedMethods("GET", "POST", "OPTIONS")
+ .allowedHeaders("Authorization", "Content-Type")
+ .maxAge(600)
+ }
+
+ override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
+ val codecs = configurer.defaultCodecs()
+ codecs.kotlinSerializationJsonDecoder(KotlinSerializationJsonDecoder(GlobalVal.JSON_FORMAT))
+ codecs.kotlinSerializationJsonEncoder(KotlinSerializationJsonEncoder(GlobalVal.JSON_FORMAT))
+ }
+}
diff --git a/server/src/main/kotlin/org/dreamexposure/discal/server/endpoints/v3/InviteEndpoint.kt b/server/src/main/kotlin/org/dreamexposure/discal/server/endpoints/v3/InviteEndpoint.kt
index ff5ea833b..6cf9cdbae 100644
--- a/server/src/main/kotlin/org/dreamexposure/discal/server/endpoints/v3/InviteEndpoint.kt
+++ b/server/src/main/kotlin/org/dreamexposure/discal/server/endpoints/v3/InviteEndpoint.kt
@@ -1,7 +1,7 @@
package org.dreamexposure.discal.server.endpoints.v3
-import org.dreamexposure.discal.core.`object`.BotSettings
import org.dreamexposure.discal.core.annotations.Authentication
+import org.dreamexposure.discal.core.config.Config
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@@ -13,6 +13,6 @@ class InviteEndpoint {
@Authentication(access = Authentication.AccessLevel.PUBLIC)
@GetMapping("invite", produces = ["text/plain"])
fun get(): Mono {
- return Mono.just(BotSettings.INVITE_URL.get())
+ return Mono.just(Config.URL_INVITE.getString())
}
}
diff --git a/server/src/main/kotlin/org/dreamexposure/discal/server/network/dbotsgg/UpdateDBotsData.kt b/server/src/main/kotlin/org/dreamexposure/discal/server/network/dbotsgg/UpdateDBotsData.kt
index 44d50de72..4f2c0a7de 100644
--- a/server/src/main/kotlin/org/dreamexposure/discal/server/network/dbotsgg/UpdateDBotsData.kt
+++ b/server/src/main/kotlin/org/dreamexposure/discal/server/network/dbotsgg/UpdateDBotsData.kt
@@ -2,34 +2,39 @@ package org.dreamexposure.discal.server.network.dbotsgg
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
-import org.dreamexposure.discal.core.`object`.BotSettings
+import org.dreamexposure.discal.core.config.Config
import org.dreamexposure.discal.core.logger.LOGGER
import org.dreamexposure.discal.core.utils.GlobalVal
import org.dreamexposure.discal.server.network.discal.NetworkManager
import org.json.JSONObject
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.stereotype.Component
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.time.Duration
@Component
-class UpdateDBotsData(private val networkManager: NetworkManager) : ApplicationRunner {
+@ConditionalOnProperty("bot.integrations.update-bot-sites", havingValue = "true")
+class UpdateDBotsData(
+ private val networkManager: NetworkManager,
+) : ApplicationRunner {
+ private val token = Config.SECRET_INTEGRATION_D_BOTS_GG_TOKEN.getString()
private fun update(): Mono {
return Mono.fromCallable {
val json = JSONObject()
- .put("guildCount", networkManager.getStatus().totalGuilds)
- .put("shardCount", networkManager.getStatus().expectedShardCount)
+ .put("guildCount", networkManager.getStatus().totalGuilds)
+ .put("shardCount", networkManager.getStatus().expectedShardCount)
val body = json.toString().toRequestBody(GlobalVal.JSON)
val request = Request.Builder()
- .url("https://discord.bots.gg/api/v1/bots/265523588918935552/stats")
- .post(body)
- .header("Authorization", BotSettings.D_BOTS_GG_TOKEN.get())
- .header("Content-Type", "application/json")
- .build()
+ .url("https://discord.bots.gg/api/v1/bots/265523588918935552/stats")
+ .post(body)
+ .header("Authorization", token)
+ .header("Content-Type", "application/json")
+ .build()
GlobalVal.HTTP_CLIENT.newCall(request).execute()
}.doOnNext { response ->
@@ -44,10 +49,8 @@ class UpdateDBotsData(private val networkManager: NetworkManager) : ApplicationR
}
override fun run(args: ApplicationArguments?) {
- if (BotSettings.UPDATE_SITES.get().equals("true", true)) {
- Flux.interval(Duration.ofHours(1))
- .flatMap { update() }
- .subscribe()
- }
+ Flux.interval(Duration.ofHours(1))
+ .flatMap { update() }
+ .subscribe()
}
}
diff --git a/server/src/main/kotlin/org/dreamexposure/discal/server/network/discal/DatabaseMigrationRunner.kt b/server/src/main/kotlin/org/dreamexposure/discal/server/network/discal/DatabaseMigrationRunner.kt
deleted file mode 100644
index 6930cc531..000000000
--- a/server/src/main/kotlin/org/dreamexposure/discal/server/network/discal/DatabaseMigrationRunner.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-package org.dreamexposure.discal.server.network.discal
-
-import com.zaxxer.hikari.HikariDataSource
-import org.dreamexposure.discal.core.`object`.BotSettings.*
-import org.dreamexposure.discal.core.logger.LOGGER
-import org.dreamexposure.discal.core.utils.GlobalVal.DEFAULT
-import org.flywaydb.core.Flyway
-import org.flywaydb.core.api.exception.FlywayValidateException
-import org.flywaydb.core.internal.command.DbMigrate
-import org.springframework.boot.ApplicationArguments
-import org.springframework.boot.ApplicationRunner
-import org.springframework.stereotype.Component
-import kotlin.system.exitProcess
-
-@Component
-class DatabaseMigrationRunner : ApplicationRunner {
- override fun run(args: ApplicationArguments?) {
- val placeholders: Map = mapOf(Pair("prefix", SQL_PREFIX.get()))
- try {
- val source = HikariDataSource()
- source.jdbcUrl = "jdbc:mysql://${SQL_HOST.get()}:${SQL_PORT.get()}/${SQL_DB.get()}"
- source.username = SQL_USER.get()
- source.password = SQL_PASS.get()
-
- val flyway = Flyway.configure()
- .dataSource(source)
- .cleanDisabled(true)
- .baselineOnMigrate(true)
- .table("${SQL_PREFIX.get()}schema_history")
- .placeholders(placeholders)
- .load()
-
- //Validate?
- flyway.validateWithResult().invalidMigrations.forEach { result ->
- LOGGER.warn("Invalid Migration: ${result.filepath}")
- LOGGER.debug("Version: ${result.version}")
- LOGGER.debug("Description: ${result.description}")
- LOGGER.debug("Details: ${result.errorDetails.errorMessage}")
- }
-
- var sm = 0
- if (args!!.containsOption("--repair"))
- flyway.repair()
- else
- sm = flyway.migrate().migrationsExecuted
-
- source.close()
- LOGGER.info(DEFAULT, "Migrations successful | $sm migrations applied!")
- } catch (e: FlywayValidateException) {
- LOGGER.error(DEFAULT, "Migrations failure (validate)", e)
- exitProcess(3)
- } catch (e: DbMigrate.FlywayMigrateException) {
- LOGGER.error(DEFAULT, "Migrations failure", e)
- exitProcess(3)
- } catch (e: Exception) {
- LOGGER.error(DEFAULT, "Migrations failures", e)
- exitProcess(2)
- }
- }
-}
diff --git a/server/src/main/kotlin/org/dreamexposure/discal/server/network/discal/NetworkManager.kt b/server/src/main/kotlin/org/dreamexposure/discal/server/network/discal/NetworkManager.kt
index 2c04cd5d4..bdd62e1e3 100644
--- a/server/src/main/kotlin/org/dreamexposure/discal/server/network/discal/NetworkManager.kt
+++ b/server/src/main/kotlin/org/dreamexposure/discal/server/network/discal/NetworkManager.kt
@@ -1,9 +1,9 @@
package org.dreamexposure.discal.server.network.discal
import org.dreamexposure.discal.Application
+import org.dreamexposure.discal.core.config.Config
import org.dreamexposure.discal.core.database.DatabaseManager
import org.dreamexposure.discal.core.logger.LOGGER
-import org.dreamexposure.discal.core.`object`.BotSettings
import org.dreamexposure.discal.core.`object`.network.discal.BotInstanceData
import org.dreamexposure.discal.core.`object`.network.discal.InstanceData
import org.dreamexposure.discal.core.`object`.network.discal.NetworkData
@@ -71,7 +71,7 @@ class NetworkManager : ApplicationRunner {
//Gotta actually see if it needs to be restarted
- if (!BotSettings.USE_RESTART_SERVICE.get().equals("true", true)) {
+ if (!Config.RESTART_SERVICE_ENABLED.getBoolean()) {
status.botStatus.removeIf { it.shardIndex == bot.shardIndex }
LOGGER.warn(STATUS, "Client disconnected from network | Index: ${bot.shardIndex} | Reason: Restart service not active!")
} else {
@@ -84,7 +84,7 @@ class NetworkManager : ApplicationRunner {
//Gotta actually see if it needs to be restarted
- if (!BotSettings.USE_RESTART_SERVICE.get().equals("true", true)) {
+ if (!Config.RESTART_SERVICE_ENABLED.getBoolean()) {
status.camStatus.removeIf { it.instanceId == cam.instanceId }
LOGGER.warn(STATUS, "Cam disconnected from network | Id: ${cam.instanceId} | Reason: Restart service not active!")
} else {
@@ -100,7 +100,7 @@ class NetworkManager : ApplicationRunner {
val bot = Flux.from { status.botStatus }
.filter { Instant.now().isAfter(it.instanceData.lastHeartbeat.plus(5, ChronoUnit.MINUTES)) }
.flatMap(this::doRestartBot)
- val cam = Flux.from { status.botStatus }
+ val cam = Flux.from { status.camStatus }
.filter { Instant.now().isAfter(it.lastHeartbeat.plus(5, ChronoUnit.MINUTES)) }
.flatMap(this::doRestartCam)
diff --git a/server/src/main/kotlin/org/dreamexposure/discal/server/network/discord/GlobalCommandRegistrar.kt b/server/src/main/kotlin/org/dreamexposure/discal/server/network/discord/GlobalCommandRegistrar.kt
index 3b55cb40c..2ea28bcb3 100644
--- a/server/src/main/kotlin/org/dreamexposure/discal/server/network/discord/GlobalCommandRegistrar.kt
+++ b/server/src/main/kotlin/org/dreamexposure/discal/server/network/discord/GlobalCommandRegistrar.kt
@@ -1,11 +1,11 @@
package org.dreamexposure.discal.server.network.discord
+import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
-import discord4j.common.JacksonResources
import discord4j.discordjson.json.ApplicationCommandRequest
import discord4j.rest.RestClient
-import org.dreamexposure.discal.core.`object`.BotSettings.DEFAULT_WEBHOOK
import org.dreamexposure.discal.core.logger.LOGGER
+import org.dreamexposure.discal.core.utils.GlobalVal.DEFAULT
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
@@ -13,25 +13,24 @@ import org.springframework.stereotype.Component
@Component
class GlobalCommandRegistrar(
- private val restClient: RestClient
+ private val restClient: RestClient,
+ private val objectMapper: ObjectMapper,
) : ApplicationRunner {
override fun run(args: ApplicationArguments?) {
- val d4jMapper = JacksonResources.create()
-
val matcher = PathMatchingResourcePatternResolver()
val applicationService = restClient.applicationService
val applicationId = restClient.applicationId.block()!!
val commands = mutableListOf()
- for (res in matcher.getResources("commands/*.json")) {
- val request = d4jMapper.objectMapper.readValue(res.inputStream)
+ for (res in matcher.getResources("commands/global/*.json")) {
+ val request = objectMapper.readValue(res.inputStream)
commands.add(request)
}
applicationService.bulkOverwriteGlobalApplicationCommand(applicationId, commands)
.doOnNext { LOGGER.debug("Bulk overwrite read: ${it.name()}") }
- .doOnError { LOGGER.error(DEFAULT_WEBHOOK.get(), "Bulk overwrite failed", it) }
+ .doOnError { LOGGER.error(DEFAULT, "Bulk overwrite failed", it) }
.subscribe()
}
}
diff --git a/server/src/main/kotlin/org/dreamexposure/discal/server/utils/Authentication.kt b/server/src/main/kotlin/org/dreamexposure/discal/server/utils/Authentication.kt
index ecad4de69..e50d56d2a 100644
--- a/server/src/main/kotlin/org/dreamexposure/discal/server/utils/Authentication.kt
+++ b/server/src/main/kotlin/org/dreamexposure/discal/server/utils/Authentication.kt
@@ -1,9 +1,9 @@
package org.dreamexposure.discal.server.utils
-import org.dreamexposure.discal.core.`object`.BotSettings
-import org.dreamexposure.discal.core.`object`.web.AuthenticationState
+import org.dreamexposure.discal.core.config.Config
import org.dreamexposure.discal.core.database.DatabaseManager
import org.dreamexposure.discal.core.logger.LOGGER
+import org.dreamexposure.discal.core.`object`.web.AuthenticationState
import org.dreamexposure.discal.core.utils.GlobalVal
import org.dreamexposure.discal.core.utils.GlobalVal.DEFAULT
import org.springframework.web.server.ServerWebExchange
@@ -40,17 +40,18 @@ object Authentication {
val authKey = swe.request.headers["Authorization"]!![0]
return when {
- authKey == BotSettings.BOT_API_TOKEN.get() -> { //This is from within discal network
+ authKey == Config.SECRET_DISCAL_API_KEY.getString() -> { //This is from within discal network
Mono.just(AuthenticationState(true)
- .status(GlobalVal.STATUS_SUCCESS)
- .reason("Success")
- .keyUsed(authKey)
- .fromDisCalNetwork(true)
+ .status(GlobalVal.STATUS_SUCCESS)
+ .reason("Success")
+ .keyUsed(authKey)
+ .fromDisCalNetwork(true)
)
}
+
tempKeys.containsKey(authKey) -> { //Temp key granted for logged in user
Mono.just(AuthenticationState(true)
- .status(GlobalVal.STATUS_SUCCESS)
+ .status(GlobalVal.STATUS_SUCCESS)
.reason("Success")
.keyUsed(authKey)
.fromDisCalNetwork(false)
diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties
deleted file mode 100644
index 462060068..000000000
--- a/server/src/main/resources/application.properties
+++ /dev/null
@@ -1,4 +0,0 @@
-spring.application.name=DisCal Server
-server.port=8080
-spring.session.store-type=redis
-discal.security.enabled=true
diff --git a/settings.gradle.kts b/settings.gradle.kts
index dd385541a..6b5325260 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,22 +1,28 @@
pluginManagement {
val kotlinVersion: String by settings
val springVersion: String by settings
- val gitPropsVersion: String by settings
+ val gitPropertiesVersion: String by settings
val jibVersion: String by settings
+ val springDependencyManagementVersion: String by settings
+
+ repositories {
+ gradlePluginPortal()
+ }
plugins {
+ // Kotlin
kotlin("jvm") version kotlinVersion
kotlin("plugin.serialization") version kotlinVersion
- kotlin("plugin.spring") version kotlinVersion
id("org.jetbrains.kotlin.plugin.allopen") version kotlinVersion
+ // Spring
+ kotlin("plugin.spring") version kotlinVersion
+ id("io.spring.dependency-management") version springDependencyManagementVersion
id("org.springframework.boot") version springVersion apply false
- id("com.gorylenko.gradle-git-properties") version gitPropsVersion apply false
- id("com.google.cloud.tools.jib") version jibVersion apply false
- }
- repositories {
- gradlePluginPortal()
+ // Tooling
+ id("com.gorylenko.gradle-git-properties") version gitPropertiesVersion apply false
+ id("com.google.cloud.tools.jib") version jibVersion apply false
}
}
diff --git a/web/build.gradle.kts b/web/build.gradle.kts
index c00d3995b..e8720c665 100644
--- a/web/build.gradle.kts
+++ b/web/build.gradle.kts
@@ -2,35 +2,32 @@ import org.apache.tools.ant.taskdefs.condition.Os
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
+ // Kotlin
+ id("org.jetbrains.kotlin.plugin.allopen")
+
+ // Spring
kotlin("plugin.spring")
id("org.springframework.boot")
- id("org.jetbrains.kotlin.plugin.allopen")
+ //id("io.spring.dependency-management")
+
+ // Tooling
id("com.google.cloud.tools.jib")
}
+// Versions -- found in gradle.properties
+// Thymeleaf
val thymeleafVersion: String by properties
-val thymeleafSecurityVersion: String by properties
-val thymeleafLayoutVersion: String by properties
-
-val springVersion: String by properties
-val springSecurityVersion: String by properties
-val springSessionVersion: String by properties
-val springR2Version: String by properties
dependencies {
api(project(":core"))
+ // Spring
+ implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
+
//Thymeleaf
- implementation("org.thymeleaf:thymeleaf:$thymeleafVersion")
+ implementation("org.thymeleaf:thymeleaf")
implementation("org.thymeleaf:thymeleaf-spring5:$thymeleafVersion")
- implementation("nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:$thymeleafLayoutVersion")
- implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity5:$thymeleafSecurityVersion")
-
- //Spring
- implementation("org.springframework.boot:spring-boot-starter-thymeleaf:$springVersion")
- implementation("org.springframework.session:spring-session-data-redis:$springSessionVersion")
- implementation("org.springframework.security:spring-security-core:$springSecurityVersion")
- implementation("org.springframework.security:spring-security-web:$springSecurityVersion")
+ implementation("nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect")
}
kotlin {
@@ -50,14 +47,13 @@ sourceSets {
}
jib {
- var imageVersion = version.toString()
- if (imageVersion.contains("SNAPSHOT")) imageVersion = "latest"
+ to {
+ image = "rg.nl-ams.scw.cloud/dreamexposure/discal-web"
+ tags = mutableSetOf("latest", version.toString())
+ }
- to.image = "rg.nl-ams.scw.cloud/dreamexposure/discal-web:$imageVersion"
val baseImage: String by properties
from.image = baseImage
-
- container.creationTime = "USE_CURRENT_TIMESTAMP"
}
// The weird OS checks are because of windows. See this SO answer: https://stackoverflow.com/a/53428540
@@ -74,6 +70,7 @@ tasks {
}
create("cleanWeb") {
+ dependsOn("npm")
var gulp = "gulp"
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
gulp = "gulp.cmd"
@@ -82,6 +79,7 @@ tasks {
}
create("compileCSS") {
+ dependsOn("npm")
var gulp = "gulp"
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
gulp = "gulp.cmd"
@@ -91,6 +89,7 @@ tasks {
}
create("compileTypescript") {
+ dependsOn("npm")
var webpack = "webpack"
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
webpack = "webpack.cmd"
@@ -106,7 +105,7 @@ tasks {
}
withType {
- dependsOn("compileCSS", "compileTypescript")
+ dependsOn("npm", "compileCSS", "compileTypescript")
}
bootJar {
diff --git a/web/src/main/html/templates/various/status.html b/web/src/main/html/templates/various/status.html
index 4321ffcf4..b1754719d 100644
--- a/web/src/main/html/templates/various/status.html
+++ b/web/src/main/html/templates/various/status.html
@@ -247,7 +247,7 @@ Network Statistics
+ th:text="'Total Memory Usage: ' + ${status.totalMemoryInGb} + 'GB'">
@@ -318,79 +318,77 @@ Central Auth
Website
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Clients
-
+
+
+
+
+ th:text="'Version: ' + ${client.instanceData.version}">
+ th:text="'Discord4J Version: ' + ${client.instanceData.d4jVersion}">
+ th:text="'Guilds: ' + ${client.guilds}">
+ th:text="'RAM Used: ' + ${client.instanceData.memory} + 'MB'">
-
+
+ Announcements: Not Yet Tracked
-
-
-
+
+
-
+
+
+
-
- Clients
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Announcements: Not Yet Tracked
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/web/src/main/kotlin/org/dreamexposure/discal/web/DisCalWeb.kt b/web/src/main/kotlin/org/dreamexposure/discal/web/DisCalWeb.kt
index 38ed7b8b4..b290fd2d2 100644
--- a/web/src/main/kotlin/org/dreamexposure/discal/web/DisCalWeb.kt
+++ b/web/src/main/kotlin/org/dreamexposure/discal/web/DisCalWeb.kt
@@ -1,15 +1,13 @@
package org.dreamexposure.discal.web
+import jakarta.annotation.PreDestroy
import org.dreamexposure.discal.Application
-import org.dreamexposure.discal.core.`object`.BotSettings
+import org.dreamexposure.discal.core.config.Config
import org.dreamexposure.discal.core.logger.LOGGER
import org.dreamexposure.discal.core.utils.GlobalVal.DEFAULT
import org.dreamexposure.discal.core.utils.GlobalVal.STATUS
import org.springframework.boot.builder.SpringApplicationBuilder
import org.springframework.stereotype.Component
-import java.io.FileReader
-import java.util.*
-import javax.annotation.PreDestroy
import kotlin.system.exitProcess
@Component
@@ -22,15 +20,11 @@ class DisCalWeb {
companion object {
@JvmStatic
fun main(args: Array) {
- //Get settings
- val p = Properties()
- p.load(FileReader("settings.properties"))
- BotSettings.init(p)
+ Config.init()
//Start up spring
try {
SpringApplicationBuilder(Application::class.java)
- .profiles(BotSettings.PROFILE.get())
.build()
.run(*args)
} catch (e: Exception) {
diff --git a/web/src/main/kotlin/org/dreamexposure/discal/web/config/WebFluxConfig.kt b/web/src/main/kotlin/org/dreamexposure/discal/web/config/WebFluxConfig.kt
index 653d0fd8c..e489bd953 100644
--- a/web/src/main/kotlin/org/dreamexposure/discal/web/config/WebFluxConfig.kt
+++ b/web/src/main/kotlin/org/dreamexposure/discal/web/config/WebFluxConfig.kt
@@ -1,6 +1,5 @@
package org.dreamexposure.discal.web.config
-import org.dreamexposure.discal.core.`object`.BotSettings
import org.dreamexposure.discal.core.utils.GlobalVal
import org.springframework.boot.web.server.ConfigurableWebServerFactory
import org.springframework.boot.web.server.ErrorPage
@@ -10,9 +9,6 @@ import org.springframework.context.ApplicationContextAware
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.io.ClassPathResource
-import org.springframework.data.redis.connection.RedisPassword
-import org.springframework.data.redis.connection.RedisStandaloneConfiguration
-import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.http.HttpStatus
import org.springframework.http.codec.ServerCodecConfigurer
import org.springframework.http.codec.json.KotlinSerializationJsonDecoder
@@ -48,17 +44,6 @@ class WebFluxConfig : WebServerFactoryCustomizer,
)
}
- @Bean(name = ["redisDatasource"])
- fun redisConnectionFactory(): LettuceConnectionFactory {
- val rsc = RedisStandaloneConfiguration()
- rsc.hostName = BotSettings.REDIS_HOSTNAME.get()
- rsc.port = BotSettings.REDIS_PORT.get().toInt()
- if (BotSettings.REDIS_USE_PASSWORD.get().equals("true", true))
- rsc.password = RedisPassword.of(BotSettings.REDIS_PASSWORD.get())
-
- return LettuceConnectionFactory(rsc)
- }
-
@Bean
fun staticResourceRouter(): RouterFunction {
return RouterFunctions.resources("/**", ClassPathResource("static/"))
diff --git a/web/src/main/kotlin/org/dreamexposure/discal/web/spring/SpringController.kt b/web/src/main/kotlin/org/dreamexposure/discal/web/controllers/PageController.kt
similarity index 97%
rename from web/src/main/kotlin/org/dreamexposure/discal/web/spring/SpringController.kt
rename to web/src/main/kotlin/org/dreamexposure/discal/web/controllers/PageController.kt
index 8a2dd94f1..f80070463 100644
--- a/web/src/main/kotlin/org/dreamexposure/discal/web/spring/SpringController.kt
+++ b/web/src/main/kotlin/org/dreamexposure/discal/web/controllers/PageController.kt
@@ -1,4 +1,4 @@
-package org.dreamexposure.discal.web.spring
+package org.dreamexposure.discal.web.controllers
import org.dreamexposure.discal.web.handler.DiscordAccountHandler
import org.dreamexposure.discal.web.network.discal.StatusHandler
@@ -9,7 +9,10 @@ import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
@Controller
-class SpringController(private val accountHandler: DiscordAccountHandler) {
+class PageController(
+ private val accountHandler: DiscordAccountHandler,
+ private val statusHandler: StatusHandler,
+) {
@RequestMapping("/", "/home")
fun home(model: MutableMap, swe: ServerWebExchange): Mono {
return accountHandler.getAccount(swe)
@@ -56,7 +59,7 @@ class SpringController(private val accountHandler: DiscordAccountHandler) {
.doOnNext { model.clear() }
.doOnNext(model::putAll)
.doOnNext { model.remove("status") }
- .then(StatusHandler.getLatestStatusInfo())
+ .then(statusHandler.getLatestStatusInfo())
.doOnNext { model["status"] = it }
.thenReturn("various/status")
}
diff --git a/web/src/main/kotlin/org/dreamexposure/discal/web/handler/DiscordAccountHandler.kt b/web/src/main/kotlin/org/dreamexposure/discal/web/handler/DiscordAccountHandler.kt
index 24650f008..8b3397801 100644
--- a/web/src/main/kotlin/org/dreamexposure/discal/web/handler/DiscordAccountHandler.kt
+++ b/web/src/main/kotlin/org/dreamexposure/discal/web/handler/DiscordAccountHandler.kt
@@ -3,7 +3,8 @@ package org.dreamexposure.discal.web.handler
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
-import org.dreamexposure.discal.core.`object`.BotSettings
+import org.dreamexposure.discal.core.config.Config
+import org.dreamexposure.discal.core.extensions.asMinutes
import org.dreamexposure.discal.core.logger.LOGGER
import org.dreamexposure.discal.core.utils.GlobalVal
import org.json.JSONObject
@@ -14,6 +15,8 @@ import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
+import java.net.URLEncoder
+import java.nio.charset.Charset
import java.time.Duration
import java.time.LocalDate
import java.util.*
@@ -21,7 +24,11 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
@Component
-class DiscordAccountHandler: ApplicationRunner {
+class DiscordAccountHandler : ApplicationRunner {
+ private final val apiUrl = Config.URL_API.getString()
+ private final val redirectUrl = Config.URL_DISCORD_REDIRECT.getString()
+ private final val clientId = Config.DISCORD_APP_ID.getString()
+
private val discordAccounts: ConcurrentMap> = ConcurrentHashMap()
fun hasAccount(swe: ServerWebExchange): Mono {
@@ -108,14 +115,14 @@ class DiscordAccountHandler: ApplicationRunner {
}
private fun removeTimedOutAccounts() {
- val limit = BotSettings.TIME_OUT.get().toLong()
+ val limit = Config.CACHE_TTL_ACCOUNTS_MINUTES.getLong().asMinutes()
val toRemove = mutableListOf()
for (id in discordAccounts.keys) {
val model = discordAccounts[id]!!
val lastUse = model["last_use"] as Long
- if (System.currentTimeMillis() - lastUse > limit)
+ if (System.currentTimeMillis() - lastUse > limit.toMillis())
toRemove.add(id) //Timed out, remove account info and require sign in
}
@@ -127,12 +134,12 @@ class DiscordAccountHandler: ApplicationRunner {
internal fun createDefaultModel(): MutableMap {
val model: MutableMap = mutableMapOf()
model["logged_in"] = false
- model["bot_id"] = BotSettings.ID.get()
+ model["bot_id"] = clientId
model["year"] = LocalDate.now().year
- model["redirect_uri"] = BotSettings.REDIR_URI.get()
- model["bot_invite"] = BotSettings.INVITE_URL.get()
- model["support_invite"] = BotSettings.SUPPORT_INVITE.get()
- model["api_url"] = BotSettings.API_URL.get()
+ model["redirect_uri"] = URLEncoder.encode(redirectUrl, Charset.defaultCharset())
+ model["bot_invite"] = Config.URL_INVITE.getString()
+ model["support_invite"] = Config.URL_SUPPORT.getString()
+ model["api_url"] = apiUrl
return model
}
@@ -142,8 +149,8 @@ class DiscordAccountHandler: ApplicationRunner {
val client = OkHttpClient()
val keyGrantRequestBody = "".toRequestBody(GlobalVal.JSON)
val keyGrantRequest = Request.Builder()
- .url("${BotSettings.API_URL.get()}/v2/account/key/readonly/get")
- .header("Authorization", BotSettings.BOT_API_TOKEN.get())
+ .url("$apiUrl/v2/account/key/readonly/get")
+ .header("Authorization", Config.SECRET_DISCAL_API_KEY.getString())
.post(keyGrantRequestBody)
.build()
diff --git a/web/src/main/kotlin/org/dreamexposure/discal/web/network/discal/HeartbeatService.kt b/web/src/main/kotlin/org/dreamexposure/discal/web/network/discal/HeartbeatService.kt
index 7541bec7f..01c303786 100644
--- a/web/src/main/kotlin/org/dreamexposure/discal/web/network/discal/HeartbeatService.kt
+++ b/web/src/main/kotlin/org/dreamexposure/discal/web/network/discal/HeartbeatService.kt
@@ -4,11 +4,11 @@ import kotlinx.serialization.encodeToString
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
-import org.dreamexposure.discal.core.`object`.BotSettings
+import org.dreamexposure.discal.core.config.Config
+import org.dreamexposure.discal.core.logger.LOGGER
import org.dreamexposure.discal.core.`object`.network.discal.InstanceData
import org.dreamexposure.discal.core.`object`.rest.HeartbeatRequest
import org.dreamexposure.discal.core.`object`.rest.HeartbeatType
-import org.dreamexposure.discal.core.logger.LOGGER
import org.dreamexposure.discal.core.utils.GlobalVal
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
@@ -20,6 +20,7 @@ import java.time.Duration
@Component
class HeartbeatService : ApplicationRunner {
+ private final val apiUrl = Config.URL_API.getString()
private fun heartbeat(): Mono {
return Mono.just(InstanceData())
@@ -29,9 +30,9 @@ class HeartbeatService : ApplicationRunner {
val body = GlobalVal.JSON_FORMAT.encodeToString(requestBody).toRequestBody(GlobalVal.JSON)
Request.Builder()
- .url("${BotSettings.API_URL.get()}/v2/status/heartbeat")
+ .url("$apiUrl/v2/status/heartbeat")
.post(body)
- .header("Authorization", BotSettings.BOT_API_TOKEN.get())
+ .header("Authorization", Config.SECRET_DISCAL_API_KEY.getString())
.header("Content-Type", "application/json")
.build()
}.flatMap {
diff --git a/web/src/main/kotlin/org/dreamexposure/discal/web/network/discal/StatusHandler.kt b/web/src/main/kotlin/org/dreamexposure/discal/web/network/discal/StatusHandler.kt
index b426a5eec..34fc6acfb 100644
--- a/web/src/main/kotlin/org/dreamexposure/discal/web/network/discal/StatusHandler.kt
+++ b/web/src/main/kotlin/org/dreamexposure/discal/web/network/discal/StatusHandler.kt
@@ -4,23 +4,27 @@ import com.google.api.client.http.HttpStatusCodes
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
-import org.dreamexposure.discal.core.`object`.BotSettings
-import org.dreamexposure.discal.core.`object`.network.discal.NetworkData
+import org.dreamexposure.discal.core.config.Config
import org.dreamexposure.discal.core.logger.LOGGER
+import org.dreamexposure.discal.core.`object`.network.discal.NetworkData
import org.dreamexposure.discal.core.utils.GlobalVal.JSON
import org.dreamexposure.discal.core.utils.GlobalVal.JSON_FORMAT
+import org.springframework.stereotype.Component
import reactor.core.publisher.Mono
-object StatusHandler {
+@Component
+class StatusHandler {
+ private final val apiUrl = Config.URL_API.getString()
+
fun getLatestStatusInfo(): Mono {
return Mono.fromCallable {
val client = OkHttpClient()
val body = "".toRequestBody(JSON)
val request = Request.Builder()
- .url("${BotSettings.API_URL.get()}/v2/status/get")
- .post(body)
- .header("Authorization", BotSettings.BOT_API_TOKEN.get())
+ .url("$apiUrl/v2/status/get")
+ .post(body)
+ .header("Authorization", Config.SECRET_DISCAL_API_KEY.getString())
.header("Content-Type", "application/json")
.build()
diff --git a/web/src/main/kotlin/org/dreamexposure/discal/web/network/discord/DiscordLoginHandler.kt b/web/src/main/kotlin/org/dreamexposure/discal/web/network/discord/DiscordLoginHandler.kt
index 4d5a7363a..d2b08b4e9 100644
--- a/web/src/main/kotlin/org/dreamexposure/discal/web/network/discord/DiscordLoginHandler.kt
+++ b/web/src/main/kotlin/org/dreamexposure/discal/web/network/discord/DiscordLoginHandler.kt
@@ -4,11 +4,11 @@ import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
+import org.dreamexposure.discal.core.config.Config
import org.dreamexposure.discal.core.enums.announcement.AnnouncementType
import org.dreamexposure.discal.core.enums.event.EventColor
import org.dreamexposure.discal.core.enums.time.GoodTimezone
import org.dreamexposure.discal.core.logger.LOGGER
-import org.dreamexposure.discal.core.`object`.BotSettings
import org.dreamexposure.discal.core.`object`.web.WebPartialGuild
import org.dreamexposure.discal.core.utils.GlobalVal
import org.dreamexposure.discal.web.handler.DiscordAccountHandler
@@ -21,21 +21,30 @@ import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
import reactor.function.TupleUtils
+import java.net.URLEncoder
+import java.nio.charset.Charset
import java.util.*
@Controller
-class DiscordLoginHandler(private val accountHandler: DiscordAccountHandler) {
+class DiscordLoginHandler(
+ private val accountHandler: DiscordAccountHandler,
+) {
+ private final val apiUrl = Config.URL_API.getString()
+ private final val redirectUrl = Config.URL_DISCORD_REDIRECT.getString()
+ private final val clientId = Config.DISCORD_APP_ID.getString()
+ private final val clientSecret = Config.SECRET_CLIENT_SECRET.getString()
+
@GetMapping("/account/login")
fun handleDiscordCode(swe: ServerWebExchange, @RequestParam("code") code: String): Mono {
val client = OkHttpClient()
return Mono.fromCallable {
val body = FormBody.Builder()
- .addEncoded("client_id", BotSettings.ID.get())
- .addEncoded("client_secret", BotSettings.SECRET.get())
- .addEncoded("grant_type", "authorization_code")
+ .addEncoded("client_id", clientId)
+ .addEncoded("client_secret", clientSecret)
+ .addEncoded("grant_type", "authorization_code")
.addEncoded("code", code)
- .addEncoded("redirect_uri", BotSettings.REDIR_URI.get())
+ .addEncoded("redirect_uri", URLEncoder.encode(redirectUrl, Charset.defaultCharset()))
.build()
val tokenRequest = Request.Builder()
@@ -122,8 +131,8 @@ class DiscordLoginHandler(private val accountHandler: DiscordAccountHandler) {
//Do temp API key request...
val keyGrantRequestBody = "".toRequestBody(GlobalVal.JSON)
val keyGrantRequest = Request.Builder()
- .url("${BotSettings.API_URL.get()}/v2/account/login")
- .header("Authorization", BotSettings.BOT_API_TOKEN.get())
+ .url("$apiUrl/v2/account/login")
+ .header("Authorization", Config.SECRET_DISCAL_API_KEY.getString())
.post(keyGrantRequestBody)
.build()
val keyGrantResponseMono = Mono
@@ -177,7 +186,7 @@ class DiscordLoginHandler(private val accountHandler: DiscordAccountHandler) {
val client = OkHttpClient()
val logoutRequest = Request.Builder()
- .url("${BotSettings.API_URL.get()}/v2/account/logout")
+ .url("$apiUrl/v2/account/logout")
.header("Authorization", model["key"] as String)
.get()
.build()
diff --git a/web/src/main/resources/application.properties b/web/src/main/resources/application.properties
deleted file mode 100644
index bdb52268f..000000000
--- a/web/src/main/resources/application.properties
+++ /dev/null
@@ -1,3 +0,0 @@
-spring.application.name=DisCal Web
-server.port=8080
-spring.session.store-type=redis