mirror of
https://github.com/DreamExposure/DisCal-Discord-Bot.git
synced 2026-01-24 21:08:27 -06:00
v4.2.7 RC
v4.2.7 RC
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import org.gradle.api.tasks.wrapper.Wrapper.DistributionType.ALL
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
@@ -27,7 +28,7 @@ buildscript {
|
||||
allprojects {
|
||||
//Project props
|
||||
group = "org.dreamexposure.discal"
|
||||
version = "4.2.6"
|
||||
version = "4.2.7"
|
||||
description = "DisCal"
|
||||
|
||||
//Plugins
|
||||
@@ -50,6 +51,7 @@ allprojects {
|
||||
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
|
||||
|
||||
@@ -88,7 +90,7 @@ allprojects {
|
||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||
|
||||
// Database
|
||||
implementation("io.asyncer:r2dbc-mysql")
|
||||
implementation("io.asyncer:r2dbc-mysql:1.3.0") // TODO: Remove hard coded version once spring includes this in bom as it is a breaking change
|
||||
implementation("com.mysql:mysql-connector-j")
|
||||
|
||||
// Serialization
|
||||
@@ -101,6 +103,7 @@ allprojects {
|
||||
implementation("ch.qos.logback.contrib:logback-json-classic:$logbackContribVersion")
|
||||
implementation("ch.qos.logback.contrib:logback-jackson:$logbackContribVersion")
|
||||
implementation("io.micrometer:micrometer-registry-prometheus")
|
||||
implementation("io.projectreactor:reactor-core-micrometer")
|
||||
|
||||
// Google libs
|
||||
implementation("com.google.api-client:google-api-client:$googleApiClientVersion")
|
||||
@@ -111,6 +114,7 @@ allprojects {
|
||||
|
||||
// Various Libs
|
||||
implementation("com.squareup.okhttp3:okhttp")
|
||||
implementation("com.squareup.okhttp3:okhttp-coroutines:$okhttpVersion")
|
||||
implementation("io.github.furstenheim:copy_down:$copyDownVersion")
|
||||
implementation("org.jsoup:jsoup:$jsoupVersion")
|
||||
}
|
||||
@@ -135,9 +139,9 @@ allprojects {
|
||||
subprojects {
|
||||
tasks {
|
||||
withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = listOf("-Xjsr305=strict")
|
||||
jvmTarget = java.targetCompatibility.majorVersion
|
||||
compilerOptions {
|
||||
freeCompilerArgs.set(listOf("-Xjsr305=strict"))
|
||||
jvmTarget.set(JvmTarget.fromTarget(java.targetCompatibility.majorVersion))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,7 +150,7 @@ subprojects {
|
||||
tasks {
|
||||
wrapper {
|
||||
distributionType = ALL
|
||||
gradleVersion = "8.2.1"
|
||||
gradleVersion = "8.10.2"
|
||||
}
|
||||
|
||||
bootJar {
|
||||
|
||||
@@ -27,7 +27,7 @@ class Cam {
|
||||
.run(*args)
|
||||
LOGGER.info(GlobalVal.STATUS, "CAM is now online")
|
||||
} catch (e: Exception) {
|
||||
LOGGER.error(GlobalVal.DEFAULT, "'Spring error' by PANIC! at the CAM")
|
||||
LOGGER.error(GlobalVal.DEFAULT, "'Spring error' by PANIC! at the CAM", e)
|
||||
exitProcess(4)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ 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.DEFAULT
|
||||
import org.dreamexposure.discal.core.utils.GlobalVal.JSON
|
||||
import org.springframework.boot.ApplicationArguments
|
||||
import org.springframework.boot.ApplicationRunner
|
||||
@@ -31,26 +31,30 @@ class HeartbeatCronJob(
|
||||
override fun run(args: ApplicationArguments?) {
|
||||
Flux.interval(Config.HEARTBEAT_INTERVAL.getLong().asSeconds())
|
||||
.flatMap { heartbeat() }
|
||||
.doOnError { LOGGER.error(GlobalVal.DEFAULT, "[Heartbeat] Failed to heartbeat", it) }
|
||||
.doOnError { LOGGER.error(DEFAULT, "[Heartbeat] Failed to heartbeat", it) }
|
||||
.onErrorResume { Mono.empty() }
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
private fun heartbeat() = mono {
|
||||
val requestBody = HeartbeatRequest(HeartbeatType.CAM, instanceData = InstanceData())
|
||||
try {
|
||||
val requestBody = HeartbeatRequest(HeartbeatType.CAM, instanceData = InstanceData())
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("$apiUrl/v3/status/heartbeat")
|
||||
.post(objectMapper.writeValueAsString(requestBody).toRequestBody(JSON))
|
||||
.header("Authorization", "Int ${Config.SECRET_DISCAL_API_KEY.getString()}")
|
||||
.header("Content-Type", "application/json")
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
.url("$apiUrl/v3/status/heartbeat")
|
||||
.post(objectMapper.writeValueAsString(requestBody).toRequestBody(JSON))
|
||||
.header("Authorization", "Int ${Config.SECRET_DISCAL_API_KEY.getString()}")
|
||||
.header("Content-Type", "application/json")
|
||||
.build()
|
||||
|
||||
Mono.fromCallable(httpClient.newCall(request)::execute)
|
||||
.map(Response::close)
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.doOnError { LOGGER.error(GlobalVal.DEFAULT, "[Heartbeat] Failed to heartbeat", it) }
|
||||
.onErrorResume { Mono.empty() }
|
||||
.subscribe()
|
||||
Mono.fromCallable(httpClient.newCall(request)::execute)
|
||||
.map(Response::close)
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.doOnError { LOGGER.error(DEFAULT, "[Heartbeat] Failed to heartbeat", it) }
|
||||
.onErrorResume { Mono.empty() }
|
||||
.subscribe()
|
||||
} catch (ex: Exception) {
|
||||
LOGGER.error(DEFAULT, "[Heartbeat] Failed to heartbeat", ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.dreamexposure.discal.cam.business.google
|
||||
|
||||
import org.dreamexposure.discal.core.business.CalendarService
|
||||
import org.dreamexposure.discal.core.business.CredentialService
|
||||
import org.dreamexposure.discal.core.business.google.GoogleAuthApiWrapper
|
||||
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`.new.CalendarMetadata
|
||||
import org.dreamexposure.discal.core.`object`.new.model.discal.cam.TokenV1Model
|
||||
import org.springframework.stereotype.Component
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@Component
|
||||
class GoogleAuthService(
|
||||
private val credentialService: CredentialService,
|
||||
private val calendarService: CalendarService,
|
||||
private val googleAuthApiWrapper: GoogleAuthApiWrapper,
|
||||
) {
|
||||
suspend fun requestNewAccessToken(calendar: CalendarMetadata): TokenV1Model? {
|
||||
if (!calendar.secrets.expiresAt.isExpiredTtl()) return TokenV1Model(calendar.secrets.accessToken, calendar.secrets.expiresAt)
|
||||
|
||||
LOGGER.debug("Refreshing access token | guildId:{} | calendar:{}", calendar.guildId, calendar.number)
|
||||
|
||||
val refreshed = googleAuthApiWrapper.refreshAccessToken(calendar.secrets.refreshToken).entity ?: return null
|
||||
calendar.secrets.accessToken = refreshed.accessToken
|
||||
calendar.secrets.expiresAt = Instant.now().plusSeconds(refreshed.expiresIn.toLong()).minus(Duration.ofMinutes(5)) // Add some wiggle room
|
||||
calendarService.updateCalendarMetadata(calendar)
|
||||
|
||||
LOGGER.debug("Refreshed access token | guildId:{} | calendar:{}, validUntil:{}", calendar.guildId, calendar.number, calendar.external)
|
||||
|
||||
return TokenV1Model(calendar.secrets.accessToken, calendar.secrets.expiresAt)
|
||||
}
|
||||
|
||||
suspend fun requestNewAccessToken(credentialId: Int): TokenV1Model {
|
||||
val credential = credentialService.getCredential(credentialId) ?: throw NotFoundException()
|
||||
if (!credential.expiresAt.isExpiredTtl()) return TokenV1Model(credential.accessToken, credential.expiresAt)
|
||||
|
||||
LOGGER.debug("Refreshing access token | credentialId:$credentialId")
|
||||
|
||||
val refreshed = googleAuthApiWrapper.refreshAccessToken(credential.refreshToken).entity ?: throw EmptyNotAllowedException()
|
||||
credential.accessToken = refreshed.accessToken
|
||||
credential.expiresAt = Instant.now().plusSeconds(refreshed.expiresIn.toLong()).minus(Duration.ofMinutes(5)) // Add some wiggle room
|
||||
credentialService.updateCredential(credential)
|
||||
|
||||
LOGGER.debug("Refreshed access token | credentialId:{} | validUntil:{}", credentialId, credential.expiresAt)
|
||||
|
||||
return TokenV1Model(credential.accessToken, credential.expiresAt)
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,10 @@ package org.dreamexposure.discal.cam.controllers.v1
|
||||
|
||||
import org.dreamexposure.discal.cam.business.SecurityService
|
||||
import org.dreamexposure.discal.core.annotations.SecurityRequirement
|
||||
import org.dreamexposure.discal.core.`object`.new.model.discal.cam.SecurityValidateV1Request
|
||||
import org.dreamexposure.discal.core.`object`.new.model.discal.cam.SecurityValidateV1Response
|
||||
import org.dreamexposure.discal.core.`object`.new.security.Scope.INTERNAL_CAM_VALIDATE_TOKEN
|
||||
import org.dreamexposure.discal.core.`object`.new.security.TokenType.INTERNAL
|
||||
import org.dreamexposure.discal.core.`object`.rest.v1.security.ValidateRequest
|
||||
import org.dreamexposure.discal.core.`object`.rest.v1.security.ValidateResponse
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
@@ -19,13 +19,13 @@ class SecurityController(
|
||||
) {
|
||||
@SecurityRequirement(schemas = [INTERNAL], scopes = [INTERNAL_CAM_VALIDATE_TOKEN])
|
||||
@PostMapping("/validate", produces = ["application/json"])
|
||||
suspend fun validate(@RequestBody request: ValidateRequest): ValidateResponse {
|
||||
suspend fun validate(@RequestBody request: SecurityValidateV1Request): SecurityValidateV1Response {
|
||||
val result = securityService.authenticateAndAuthorizeToken(
|
||||
request.token,
|
||||
request.schemas,
|
||||
request.scopes,
|
||||
)
|
||||
|
||||
return ValidateResponse(result.first == HttpStatus.OK, result.first, result.second)
|
||||
return SecurityValidateV1Response(result.first == HttpStatus.OK, result.first, result.second)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import discord4j.common.util.Snowflake
|
||||
import org.dreamexposure.discal.cam.managers.CalendarAuthManager
|
||||
import org.dreamexposure.discal.core.annotations.SecurityRequirement
|
||||
import org.dreamexposure.discal.core.enums.calendar.CalendarHost
|
||||
import org.dreamexposure.discal.core.`object`.network.discal.CredentialData
|
||||
import org.dreamexposure.discal.core.`object`.new.model.discal.cam.TokenV1Model
|
||||
import org.dreamexposure.discal.core.`object`.new.security.Scope.CALENDAR_TOKEN_READ
|
||||
import org.dreamexposure.discal.core.`object`.new.security.TokenType.INTERNAL
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
@@ -19,7 +19,7 @@ class TokenController(
|
||||
) {
|
||||
@SecurityRequirement(schemas = [INTERNAL], scopes = [CALENDAR_TOKEN_READ])
|
||||
@GetMapping(produces = ["application/json"])
|
||||
suspend fun getToken(@RequestParam host: CalendarHost, @RequestParam id: Int, @RequestParam guild: Snowflake?): CredentialData? {
|
||||
suspend fun getToken(@RequestParam host: CalendarHost, @RequestParam id: Int, @RequestParam guild: Snowflake?): TokenV1Model? {
|
||||
return calendarAuthManager.getCredentialData(host, id, guild)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
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.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.dreamexposure.discal.cam.json.google.ErrorData
|
||||
import org.dreamexposure.discal.cam.json.google.RefreshData
|
||||
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.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.springframework.stereotype.Component
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.core.scheduler.Schedulers
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@Component
|
||||
class GoogleAuth(
|
||||
private val credentialService: CredentialService,
|
||||
private val calendarService: CalendarService,
|
||||
private val objectMapper: ObjectMapper,
|
||||
private val httpClient: OkHttpClient,
|
||||
) {
|
||||
|
||||
suspend fun requestNewAccessToken(calendar: Calendar): CredentialData? {
|
||||
if (!calendar.secrets.expiresAt.isExpiredTtl()) return CredentialData(calendar.secrets.accessToken, calendar.secrets.expiresAt)
|
||||
|
||||
LOGGER.debug("Refreshing access token | guildId:{} | calendar:{}", calendar.guildId, calendar.number)
|
||||
|
||||
val refreshedCredential = doAccessTokenRequest(calendar.secrets.refreshToken) ?: return null
|
||||
calendar.secrets.accessToken = refreshedCredential.accessToken
|
||||
calendar.secrets.expiresAt = refreshedCredential.validUntil.minus(Duration.ofMinutes(5)) // Add some wiggle room
|
||||
calendarService.updateCalendar(calendar)
|
||||
|
||||
LOGGER.debug("Refreshing access token | guildId:{} | calendar:{}", calendar.guildId, calendar.number)
|
||||
|
||||
return refreshedCredential
|
||||
}
|
||||
|
||||
suspend fun requestNewAccessToken(credentialId: Int): CredentialData {
|
||||
val credential = credentialService.getCredential(credentialId) ?: throw NotFoundException()
|
||||
if (!credential.expiresAt.isExpiredTtl()) return CredentialData(credential.accessToken, credential.expiresAt)
|
||||
|
||||
LOGGER.debug("Refreshing access token | credentialId:$credentialId")
|
||||
|
||||
val refreshedCredentialData = doAccessTokenRequest(credential.refreshToken) ?: throw EmptyNotAllowedException()
|
||||
credential.accessToken = refreshedCredentialData.accessToken
|
||||
credential.expiresAt = refreshedCredentialData.validUntil.minus(Duration.ofMinutes(5)) // Add some wiggle room
|
||||
credentialService.updateCredential(credential)
|
||||
|
||||
LOGGER.debug("Refreshed access token | credentialId:{} | validUntil{}", credentialId, credential.expiresAt)
|
||||
|
||||
return refreshedCredentialData
|
||||
}
|
||||
|
||||
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(httpClient.newCall(request)::execute)
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.awaitSingle()
|
||||
|
||||
return when (response.code) {
|
||||
STATUS_CODE_OK -> {
|
||||
val body = objectMapper.readValue<RefreshData>(response.body!!.string())
|
||||
response.close()
|
||||
|
||||
CredentialData(body.accessToken, Instant.now().plusSeconds(body.expiresIn.toLong()))
|
||||
}
|
||||
STATUS_CODE_BAD_REQUEST -> {
|
||||
val bodyRaw = response.body!!.string()
|
||||
LOGGER.error("[Google] Access Token Request: $bodyRaw")
|
||||
val body = objectMapper.readValue<ErrorData>(bodyRaw)
|
||||
response.close()
|
||||
|
||||
|
||||
if (body.error == "invalid_grant") {
|
||||
LOGGER.debug(DEFAULT, "[Google] Access to resource has been revoked")
|
||||
throw AccessRevokedException() // TODO: How should I handle this for external calendars? Right now we just delete everything
|
||||
} 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package org.dreamexposure.discal.cam.json.google
|
||||
|
||||
|
||||
data class ErrorData(
|
||||
val error: String
|
||||
)
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.dreamexposure.discal.cam.json.google
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
data class RefreshData(
|
||||
@JsonProperty("access_token")
|
||||
val accessToken: String,
|
||||
|
||||
@JsonProperty("expires_in")
|
||||
val expiresIn: Int
|
||||
)
|
||||
@@ -1,29 +1,29 @@
|
||||
package org.dreamexposure.discal.cam.managers
|
||||
|
||||
import discord4j.common.util.Snowflake
|
||||
import org.dreamexposure.discal.cam.google.GoogleAuth
|
||||
import org.dreamexposure.discal.cam.business.google.GoogleAuthService
|
||||
import org.dreamexposure.discal.core.business.CalendarService
|
||||
import org.dreamexposure.discal.core.enums.calendar.CalendarHost
|
||||
import org.dreamexposure.discal.core.logger.LOGGER
|
||||
import org.dreamexposure.discal.core.`object`.network.discal.CredentialData
|
||||
import org.dreamexposure.discal.core.`object`.new.model.discal.cam.TokenV1Model
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class CalendarAuthManager(
|
||||
private val calendarService: CalendarService,
|
||||
private val googleAuth: GoogleAuth,
|
||||
private val googleAuthService: GoogleAuthService,
|
||||
) {
|
||||
suspend fun getCredentialData(host: CalendarHost, id: Int, guild: Snowflake?): CredentialData? {
|
||||
suspend fun getCredentialData(host: CalendarHost, id: Int, guild: Snowflake?): TokenV1Model? {
|
||||
return try {
|
||||
when (host) {
|
||||
CalendarHost.GOOGLE -> {
|
||||
if (guild == null) {
|
||||
// Internal (owned by DisCal, should never go bad)
|
||||
googleAuth.requestNewAccessToken(id)
|
||||
googleAuthService.requestNewAccessToken(id)
|
||||
} else {
|
||||
// External (owned by user)
|
||||
val calendar = calendarService.getCalendar(guild, id) ?: return null
|
||||
googleAuth.requestNewAccessToken(calendar)
|
||||
val calendar = calendarService.getCalendarMetadata(guild, id) ?: return null
|
||||
googleAuthService.requestNewAccessToken(calendar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,19 +35,23 @@ class AnnouncementCronJob(
|
||||
}
|
||||
|
||||
private fun doAction() = mono {
|
||||
val taskTimer = StopWatch()
|
||||
taskTimer.start()
|
||||
try {
|
||||
val taskTimer = StopWatch()
|
||||
taskTimer.start()
|
||||
|
||||
val guilds = discordClient.guilds.collectList().awaitSingle()
|
||||
val guilds = discordClient.guilds.collectList().awaitSingle()
|
||||
|
||||
guilds.forEach { guild ->
|
||||
try {
|
||||
announcementService.processAnnouncementsForGuild(guild.id, maxDifference)
|
||||
} catch (ex: Exception) {
|
||||
LOGGER.error("Failed to process announcements for guild | guildId:${guild.id.asLong()}", ex)
|
||||
guilds.forEach { guild ->
|
||||
try {
|
||||
announcementService.processAnnouncementsForGuild(guild.id, maxDifference)
|
||||
} catch (ex: Exception) {
|
||||
LOGGER.error("Failed to process announcements for guild | guildId:${guild.id.asLong()}", ex)
|
||||
}
|
||||
}
|
||||
taskTimer.stop()
|
||||
metricService.recordAnnouncementTaskDuration("cronjob", taskTimer.totalTimeMillis)
|
||||
} catch (ex: Exception) {
|
||||
LOGGER.error(DEFAULT, "Announcement task failed for all guilds", ex)
|
||||
}
|
||||
taskTimer.stop()
|
||||
metricService.recordAnnouncementTaskDuration("cronjob", taskTimer.totalTimeMillis)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ 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.utils.GlobalVal
|
||||
import org.dreamexposure.discal.core.utils.GlobalVal.DEFAULT
|
||||
import org.springframework.boot.ApplicationArguments
|
||||
import org.springframework.boot.ApplicationRunner
|
||||
import org.springframework.stereotype.Component
|
||||
@@ -33,27 +34,31 @@ class HeartbeatCronJob(
|
||||
override fun run(args: ApplicationArguments?) {
|
||||
Flux.interval(Config.HEARTBEAT_INTERVAL.getLong().asSeconds())
|
||||
.flatMap { heartbeat() }
|
||||
.doOnError { LOGGER.error(GlobalVal.DEFAULT, "[Heartbeat] Failed to heartbeat", it) }
|
||||
.doOnError { LOGGER.error(DEFAULT, "[Heartbeat] Failed to heartbeat", it) }
|
||||
.onErrorResume { Mono.empty() }
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
private fun heartbeat() = mono {
|
||||
val data = BotInstanceData.load(discordClient).awaitSingle()
|
||||
try {
|
||||
val data = BotInstanceData.load(discordClient).awaitSingle()
|
||||
|
||||
val requestBody = HeartbeatRequest(HeartbeatType.BOT, botInstanceData = data)
|
||||
val request = Request.Builder()
|
||||
.url("$apiUrl/v3/status/heartbeat")
|
||||
.post(objectMapper.writeValueAsString(requestBody).toRequestBody(GlobalVal.JSON))
|
||||
.header("Authorization", "Int ${Config.SECRET_DISCAL_API_KEY.getString()}")
|
||||
.header("Content-Type", "application/json")
|
||||
.build()
|
||||
val requestBody = HeartbeatRequest(HeartbeatType.BOT, botInstanceData = data)
|
||||
val request = Request.Builder()
|
||||
.url("$apiUrl/v3/status/heartbeat")
|
||||
.post(objectMapper.writeValueAsString(requestBody).toRequestBody(GlobalVal.JSON))
|
||||
.header("Authorization", "Int ${Config.SECRET_DISCAL_API_KEY.getString()}")
|
||||
.header("Content-Type", "application/json")
|
||||
.build()
|
||||
|
||||
Mono.fromCallable(httpClient.newCall(request)::execute)
|
||||
.map(Response::close)
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.doOnError { LOGGER.error(GlobalVal.DEFAULT, "[Heartbeat] Failed to heartbeat", it) }
|
||||
.onErrorResume { Mono.empty() }
|
||||
.subscribe()
|
||||
Mono.fromCallable(httpClient.newCall(request)::execute)
|
||||
.map(Response::close)
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.doOnError { LOGGER.error(DEFAULT, "[Heartbeat] Failed to heartbeat", it) }
|
||||
.onErrorResume { Mono.empty() }
|
||||
.subscribe()
|
||||
} catch (ex: Exception) {
|
||||
LOGGER.error(DEFAULT, "[Heartbeat] Failed to heartbeat", ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,16 +37,16 @@ class StatusUpdateCronJob(
|
||||
"Version {version}",
|
||||
"{calendar_count} calendars managed!",
|
||||
"Now has interactions!",
|
||||
"Delay, Deny, Defend",
|
||||
"Proudly written in Kotlin using Discord4J",
|
||||
"Free Palestine!",
|
||||
"https://discalbot.com",
|
||||
"I swear DisCal isn't abandoned",
|
||||
"Powered by Discord4J v{d4j_version}",
|
||||
"{shards} total shards!",
|
||||
"Slava Ukraini!",
|
||||
"Support DisCal on Patreon",
|
||||
"{announcement_count} announcements running!",
|
||||
"Finally fixing the annoying stuff"
|
||||
"Now the real improvements begin"
|
||||
)
|
||||
|
||||
override fun run(args: ApplicationArguments?) {
|
||||
|
||||
@@ -2,29 +2,22 @@ package org.dreamexposure.discal.client.commands
|
||||
|
||||
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.`object`.new.GuildSettings
|
||||
import org.dreamexposure.discal.core.utils.MessageSourceLoader
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
interface SlashCommand {
|
||||
val name: String
|
||||
val hasSubcommands: Boolean
|
||||
val ephemeral: Boolean
|
||||
|
||||
@Deprecated("Use new handleSuspend for K-coroutines")
|
||||
fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
return mono { suspendHandle(event, settings) }
|
||||
}
|
||||
fun shouldDefer(event: ChatInputInteractionEvent): Boolean = true
|
||||
|
||||
suspend fun suspendHandle(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
return handle(event, settings).awaitSingle()
|
||||
}
|
||||
|
||||
suspend fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Message
|
||||
|
||||
fun getMessage(key: String, settings: GuildSettings, vararg args: String): String {
|
||||
val src = MessageSourceLoader.getSourceByPath("command/$name/$name")
|
||||
|
||||
return src.getMessage(key, args, settings.getLocale())
|
||||
return src.getMessage(key, args, settings.locale)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,126 +1,105 @@
|
||||
package org.dreamexposure.discal.client.commands.dev
|
||||
|
||||
import discord4j.common.util.Snowflake
|
||||
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 kotlinx.coroutines.reactor.awaitSingle
|
||||
import org.dreamexposure.discal.client.commands.SlashCommand
|
||||
import org.dreamexposure.discal.core.crypto.KeyGenerator.csRandomAlphaNumericString
|
||||
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.`object`.GuildSettings
|
||||
import org.dreamexposure.discal.core.`object`.web.UserAPIAccount
|
||||
import org.dreamexposure.discal.core.business.EmbedService
|
||||
import org.dreamexposure.discal.core.business.GuildSettingsService
|
||||
import org.dreamexposure.discal.core.`object`.new.GuildSettings
|
||||
import org.dreamexposure.discal.core.utils.GlobalVal
|
||||
import org.springframework.stereotype.Component
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
@Component
|
||||
class DevCommand : SlashCommand {
|
||||
class DevCommand(
|
||||
private val settingsService: GuildSettingsService,
|
||||
private val embedService: EmbedService,
|
||||
) : SlashCommand {
|
||||
override val name = "dev"
|
||||
override val hasSubcommands = true
|
||||
override val ephemeral = true
|
||||
|
||||
@Deprecated("Use new handleSuspend for K-coroutines")
|
||||
override fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
if (!GlobalVal.devUserIds.contains(event.interaction.user.id)) {
|
||||
return event.followupEphemeral(getMessage("error.notDeveloper", settings))
|
||||
}
|
||||
override suspend fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
// Validate this user is actually a dev
|
||||
if (!GlobalVal.devUserIds.contains(event.interaction.user.id))
|
||||
return event.createFollowup(getMessage("error.notDeveloper", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
return when (event.options[0].name) {
|
||||
"patron" -> patronSubcommand(event, settings)
|
||||
"dev" -> devSubcommand(event, settings)
|
||||
"maxcal" -> maxCalSubcommand(event, settings)
|
||||
"api-register" -> apiRegisterSubcommand(event, settings)
|
||||
"api-block" -> apiBlockSubcommand(event, settings)
|
||||
else -> Mono.empty() //Never can reach this, makes compiler happy.
|
||||
"patron" -> patron(event, settings)
|
||||
"dev" -> dev(event, settings)
|
||||
"maxcal" -> maxCalendars(event, settings)
|
||||
"settings" -> settings(event)
|
||||
else -> throw IllegalStateException("Invalid subcommand specified")
|
||||
}
|
||||
}
|
||||
|
||||
private fun patronSubcommand(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
private suspend fun patron(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
val guildId = event.options[0].getOption("guild")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asSnowflake)
|
||||
.get()
|
||||
|
||||
return DatabaseManager.getSettings(guildId)
|
||||
.doOnNext { settings.patronGuild = !settings.patronGuild }
|
||||
.flatMap {
|
||||
DatabaseManager.updateSettings(it).then(
|
||||
event.followupEphemeral(getMessage("patron.success", settings, settings.patronGuild.toString()))
|
||||
)
|
||||
}.doOnError { LOGGER.error("[cmd] patron failure", it) }
|
||||
.onErrorResume { event.followupEphemeral(getMessage("patron.failure.badId", settings)) }
|
||||
}
|
||||
|
||||
private fun devSubcommand(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
val guildId = event.options[0].getOption("guild")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asSnowflake)
|
||||
.get()
|
||||
|
||||
return DatabaseManager.getSettings(guildId)
|
||||
.doOnNext { settings.devGuild = !settings.devGuild }
|
||||
.flatMap {
|
||||
DatabaseManager.updateSettings(it).then(
|
||||
event.followupEphemeral(getMessage("dev.success", settings, settings.devGuild.toString()))
|
||||
)
|
||||
}.doOnError { LOGGER.error("[cmd] dev failure", it) }
|
||||
.onErrorResume { event.followupEphemeral(getMessage("dev.failure.badId", settings)) }
|
||||
}
|
||||
|
||||
private fun maxCalSubcommand(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
val guildId = event.options[0].getOption("guild")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asSnowflake)
|
||||
.get()
|
||||
|
||||
return DatabaseManager.getSettings(guildId)
|
||||
.doOnNext {
|
||||
val amount = event.options[0].getOption("amount").get().value.get().asLong().toInt()
|
||||
it.maxCalendars = amount
|
||||
}.flatMap {
|
||||
DatabaseManager.updateSettings(it).then(
|
||||
event.followupEphemeral(getMessage("maxcal.success", settings, settings.maxCalendars.toString()))
|
||||
)
|
||||
}
|
||||
.onErrorResume {
|
||||
event.followupEphemeral(getMessage("maxcal.failure.badInput", settings))
|
||||
}
|
||||
}
|
||||
|
||||
private fun apiRegisterSubcommand(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
return Mono.justOrEmpty(event.options[0].getOption("user").flatMap { it.value })
|
||||
.flatMap(ApplicationCommandInteractionOptionValue::asUser)
|
||||
.flatMap { user ->
|
||||
val acc = UserAPIAccount(
|
||||
user.id.asString(),
|
||||
csRandomAlphaNumericString(64),
|
||||
false,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
|
||||
DatabaseManager.updateAPIAccount(acc).flatMap { success ->
|
||||
if (success) {
|
||||
event.followupEphemeral(getMessage("apiRegister.success", settings, acc.APIKey))
|
||||
} else {
|
||||
event.followupEphemeral(getMessage("apiRegister.failure.unable", settings))
|
||||
}
|
||||
}
|
||||
}.switchIfEmpty(event.followupEphemeral(getMessage("apiRegister.failure.empty", settings)))
|
||||
}
|
||||
|
||||
private fun apiBlockSubcommand(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
return Mono.justOrEmpty(event.options[0].getOption("key").flatMap { it.value })
|
||||
.map(ApplicationCommandInteractionOptionValue::asString)
|
||||
.flatMap(DatabaseManager::getAPIAccount)
|
||||
.map {
|
||||
it.copy(blocked = true)
|
||||
}.flatMap(DatabaseManager::updateAPIAccount)
|
||||
.flatMap { event.followupEphemeral(getMessage("apiBlock.success", settings)) }
|
||||
.switchIfEmpty(event.followupEphemeral(getMessage("apiBlock.failure.notFound", settings)))
|
||||
.onErrorResume {
|
||||
event.followupEphemeral(getMessage("apiBlock.failure.other", settings))
|
||||
}
|
||||
.map(Snowflake::of)
|
||||
.get()
|
||||
|
||||
val oldTargetSettings = settingsService.getSettings(guildId)
|
||||
val newTargetSettings = settingsService.upsertSettings(oldTargetSettings.copy(patronGuild = !oldTargetSettings.patronGuild))
|
||||
|
||||
return event.createFollowup(getMessage("patron.success", settings, "${newTargetSettings.patronGuild}"))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
private suspend fun dev(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
val guildId = event.options[0].getOption("guild")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asString)
|
||||
.map(Snowflake::of)
|
||||
.get()
|
||||
|
||||
val oldTargetSettings = settingsService.getSettings(guildId)
|
||||
val newTargetSettings = settingsService.upsertSettings(oldTargetSettings.copy(devGuild = !oldTargetSettings.devGuild))
|
||||
|
||||
return event.createFollowup(getMessage("dev.success", settings, newTargetSettings.devGuild.toString()))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
private suspend fun maxCalendars(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
val guildId = event.options[0].getOption("guild")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asString)
|
||||
.map(Snowflake::of)
|
||||
.get()
|
||||
val amount = event.options[0].getOption("amount")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asLong)
|
||||
.map(Long::toInt)
|
||||
.get()
|
||||
|
||||
val oldTargetSettings = settingsService.getSettings(guildId)
|
||||
settingsService.upsertSettings(oldTargetSettings.copy(maxCalendars = amount))
|
||||
|
||||
return event.createFollowup(getMessage("maxcal.success", settings, "$amount"))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
private suspend fun settings(event: ChatInputInteractionEvent): Message {
|
||||
val guildId = event.options[0].getOption("guild")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asString)
|
||||
.map(Snowflake::of)
|
||||
.get()
|
||||
|
||||
val targetSettings = settingsService.getSettings(guildId)
|
||||
|
||||
return event.createFollowup()
|
||||
.withEmbeds(embedService.settingsEmbeds(targetSettings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,19 +6,16 @@ import discord4j.core.`object`.command.ApplicationCommandInteractionOptionValue
|
||||
import discord4j.core.`object`.entity.Message
|
||||
import discord4j.rest.util.AllowedMentions
|
||||
import kotlinx.coroutines.reactor.awaitSingle
|
||||
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
||||
import org.dreamexposure.discal.client.commands.SlashCommand
|
||||
import org.dreamexposure.discal.core.business.AnnouncementService
|
||||
import org.dreamexposure.discal.core.business.CalendarService
|
||||
import org.dreamexposure.discal.core.business.EmbedService
|
||||
import org.dreamexposure.discal.core.business.PermissionService
|
||||
import org.dreamexposure.discal.core.crypto.KeyGenerator
|
||||
import org.dreamexposure.discal.core.enums.event.EventColor
|
||||
import org.dreamexposure.discal.core.extensions.discord4j.followup
|
||||
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.`object`.GuildSettings
|
||||
import org.dreamexposure.discal.core.`object`.new.Announcement
|
||||
import org.dreamexposure.discal.core.`object`.new.AnnouncementWizardState
|
||||
import org.dreamexposure.discal.core.`object`.new.GuildSettings
|
||||
import org.dreamexposure.discal.core.utils.getCommonMsg
|
||||
import org.springframework.stereotype.Component
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
@@ -26,13 +23,15 @@ import kotlin.jvm.optionals.getOrNull
|
||||
@Component
|
||||
class AnnouncementCommand(
|
||||
private val announcementService: AnnouncementService,
|
||||
private val permissionService: PermissionService,
|
||||
private val embedService: EmbedService,
|
||||
private val calendarService: CalendarService,
|
||||
) : SlashCommand {
|
||||
override val name = "announcement"
|
||||
override val hasSubcommands = true
|
||||
override val ephemeral = true
|
||||
|
||||
override suspend fun suspendHandle(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
override suspend fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
return when (event.options[0].name) {
|
||||
"create" -> create(event, settings)
|
||||
"type" -> type(event, settings)
|
||||
@@ -86,11 +85,13 @@ class AnnouncementCommand(
|
||||
.orElse(1)
|
||||
|
||||
// Validate permissions
|
||||
val hasControlRole = event.interaction.member.get().hasControlRole().awaitSingle()
|
||||
if (!hasControlRole) return event.followup(getCommonMsg("error.perms.privileged", settings)).awaitSingle()
|
||||
val hasControlRole = permissionService.hasControlRole(settings.guildId, event.interaction.user.id)
|
||||
if (!hasControlRole) return event.createFollowup(getCommonMsg("error.perms.privileged", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
// Check if wizard already started
|
||||
val existingWizard = announcementService.getWizard(settings.guildID, event.interaction.user.id)
|
||||
val existingWizard = announcementService.getWizard(settings.guildId, event.interaction.user.id)
|
||||
if (existingWizard != null) {
|
||||
return event.createFollowup(getMessage("error.wizard.started", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
@@ -99,11 +100,11 @@ class AnnouncementCommand(
|
||||
}
|
||||
|
||||
val newWizard = AnnouncementWizardState(
|
||||
guildId = settings.guildID,
|
||||
guildId = settings.guildId,
|
||||
userId = event.interaction.user.id,
|
||||
editing = false,
|
||||
entity = Announcement(
|
||||
guildId = settings.guildID,
|
||||
guildId = settings.guildId,
|
||||
calendarNumber = calendar,
|
||||
type = type,
|
||||
channelId = channelId,
|
||||
@@ -127,15 +128,18 @@ class AnnouncementCommand(
|
||||
.get()
|
||||
|
||||
// Validate permissions
|
||||
val hasControlRole = event.interaction.member.get().hasControlRole().awaitSingle()
|
||||
if (!hasControlRole) return event.followup(getCommonMsg("error.perms.privileged", settings)).awaitSingle()
|
||||
val hasControlRole = permissionService.hasControlRole(settings.guildId, event.interaction.user.id)
|
||||
if (!hasControlRole) return event.createFollowup(getCommonMsg("error.perms.privileged", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
// Check if wizard not started
|
||||
val existingWizard = announcementService.getWizard(settings.guildID, event.interaction.user.id)
|
||||
val existingWizard = announcementService.getWizard(settings.guildId, event.interaction.user.id)
|
||||
?: return event.createFollowup(getMessage("error.wizard.notStarted", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
|
||||
val altered = existingWizard.copy(
|
||||
entity = existingWizard.entity.copy(
|
||||
type = type,
|
||||
@@ -158,11 +162,13 @@ class AnnouncementCommand(
|
||||
.get()
|
||||
|
||||
// Validate permissions
|
||||
val hasControlRole = event.interaction.member.get().hasControlRole().awaitSingle()
|
||||
if (!hasControlRole) return event.followup(getCommonMsg("error.perms.privileged", settings)).awaitSingle()
|
||||
val hasControlRole = permissionService.hasControlRole(settings.guildId, event.interaction.user.id)
|
||||
if (!hasControlRole) return event.createFollowup(getCommonMsg("error.perms.privileged", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
// Check if wizard not started
|
||||
val existingWizard = announcementService.getWizard(settings.guildID, event.interaction.user.id)
|
||||
val existingWizard = announcementService.getWizard(settings.guildId, event.interaction.user.id)
|
||||
?: return event.createFollowup(getMessage("error.wizard.notStarted", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
@@ -177,19 +183,16 @@ class AnnouncementCommand(
|
||||
}
|
||||
|
||||
// Validate event actually exists
|
||||
val calendarEvent = event.interaction.guild
|
||||
.flatMap { it.getCalendar(announcement.calendarNumber) }
|
||||
.flatMap { it.getEvent(eventId) }
|
||||
.awaitSingleOrNull()
|
||||
val calendarEvent = calendarService.getEvent(announcement.guildId, announcement.calendarNumber, eventId)
|
||||
if (calendarEvent == null) {
|
||||
return event.createFollowup(getCommonMsg("error.notFound.event", settings))
|
||||
return event.createFollowup(getCommonMsg("error.notFound.event", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.withEmbeds(embedService.announcementWizardEmbed(existingWizard, settings))
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
// Handle what format the ID is actually saved in
|
||||
val idToSet = if (announcement.type == Announcement.Type.RECUR) calendarEvent.eventId.split("_")[0]
|
||||
val idToSet = if (announcement.type == Announcement.Type.RECUR) calendarEvent.id.split("_")[0]
|
||||
else eventId
|
||||
|
||||
val alteredWizard = existingWizard.copy(entity = announcement.copy(eventId = idToSet))
|
||||
@@ -210,11 +213,13 @@ class AnnouncementCommand(
|
||||
.get()
|
||||
|
||||
// Validate permissions
|
||||
val hasControlRole = event.interaction.member.get().hasControlRole().awaitSingle()
|
||||
if (!hasControlRole) return event.followup(getCommonMsg("error.perms.privileged", settings)).awaitSingle()
|
||||
val hasControlRole = permissionService.hasControlRole(settings.guildId, event.interaction.user.id)
|
||||
if (!hasControlRole) return event.createFollowup(getCommonMsg("error.perms.privileged", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
// Check if wizard not started
|
||||
val existingWizard = announcementService.getWizard(settings.guildID, event.interaction.user.id)
|
||||
val existingWizard = announcementService.getWizard(settings.guildId, event.interaction.user.id)
|
||||
?: return event.createFollowup(getMessage("error.wizard.notStarted", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
@@ -243,11 +248,13 @@ class AnnouncementCommand(
|
||||
.orElse(event.interaction.channelId)
|
||||
|
||||
// Validate permissions
|
||||
val hasControlRole = event.interaction.member.get().hasControlRole().awaitSingle()
|
||||
if (!hasControlRole) return event.followup(getCommonMsg("error.perms.privileged", settings)).awaitSingle()
|
||||
val hasControlRole = permissionService.hasControlRole(settings.guildId, event.interaction.user.id)
|
||||
if (!hasControlRole) return event.createFollowup(getCommonMsg("error.perms.privileged", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
// Check if wizard not started
|
||||
val existingWizard = announcementService.getWizard(settings.guildID, event.interaction.user.id)
|
||||
val existingWizard = announcementService.getWizard(settings.guildId, event.interaction.user.id)
|
||||
?: return event.createFollowup(getMessage("error.wizard.notStarted", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
@@ -270,11 +277,13 @@ class AnnouncementCommand(
|
||||
.get()
|
||||
|
||||
// Validate permissions
|
||||
val hasControlRole = event.interaction.member.get().hasControlRole().awaitSingle()
|
||||
if (!hasControlRole) return event.followup(getCommonMsg("error.perms.privileged", settings)).awaitSingle()
|
||||
val hasControlRole = permissionService.hasControlRole(settings.guildId, event.interaction.user.id)
|
||||
if (!hasControlRole) return event.createFollowup(getCommonMsg("error.perms.privileged", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
// Check if wizard not started
|
||||
val existingWizard = announcementService.getWizard(settings.guildID, event.interaction.user.id)
|
||||
val existingWizard = announcementService.getWizard(settings.guildId, event.interaction.user.id)
|
||||
?: return event.createFollowup(getMessage("error.wizard.notStarted", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
@@ -297,11 +306,13 @@ class AnnouncementCommand(
|
||||
.orElse(0)
|
||||
|
||||
// Validate permissions
|
||||
val hasControlRole = event.interaction.member.get().hasControlRole().awaitSingle()
|
||||
if (!hasControlRole) return event.followup(getCommonMsg("error.perms.privileged", settings)).awaitSingle()
|
||||
val hasControlRole = permissionService.hasControlRole(settings.guildId, event.interaction.user.id)
|
||||
if (!hasControlRole) return event.createFollowup(getCommonMsg("error.perms.privileged", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
// Check if wizard not started
|
||||
val existingWizard = announcementService.getWizard(settings.guildID, event.interaction.user.id)
|
||||
val existingWizard = announcementService.getWizard(settings.guildId, event.interaction.user.id)
|
||||
?: return event.createFollowup(getMessage("error.wizard.notStarted", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
@@ -324,11 +335,13 @@ class AnnouncementCommand(
|
||||
.getOrNull()
|
||||
|
||||
// Validate permissions
|
||||
val hasControlRole = event.interaction.member.get().hasControlRole().awaitSingle()
|
||||
if (!hasControlRole) return event.followup(getCommonMsg("error.perms.privileged", settings)).awaitSingle()
|
||||
val hasControlRole = permissionService.hasControlRole(settings.guildId, event.interaction.user.id)
|
||||
if (!hasControlRole) return event.createFollowup(getCommonMsg("error.perms.privileged", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
// Check if wizard not started
|
||||
val existingWizard = announcementService.getWizard(settings.guildID, event.interaction.user.id)
|
||||
val existingWizard = announcementService.getWizard(settings.guildId, event.interaction.user.id)
|
||||
?: return event.createFollowup(getMessage("error.wizard.notStarted", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
@@ -351,11 +364,13 @@ class AnnouncementCommand(
|
||||
.get()
|
||||
|
||||
// Validate permissions
|
||||
val hasControlRole = event.interaction.member.get().hasControlRole().awaitSingle()
|
||||
if (!hasControlRole) return event.followup(getCommonMsg("error.perms.privileged", settings)).awaitSingle()
|
||||
val hasControlRole = permissionService.hasControlRole(settings.guildId, event.interaction.user.id)
|
||||
if (!hasControlRole) return event.createFollowup(getCommonMsg("error.perms.privileged", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
// Check if wizard not started
|
||||
val existingWizard = announcementService.getWizard(settings.guildID, event.interaction.user.id)
|
||||
val existingWizard = announcementService.getWizard(settings.guildId, event.interaction.user.id)
|
||||
?: return event.createFollowup(getMessage("error.wizard.notStarted", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
@@ -377,11 +392,13 @@ class AnnouncementCommand(
|
||||
.get()
|
||||
|
||||
// Validate permissions
|
||||
val hasControlRole = event.interaction.member.get().hasControlRole().awaitSingle()
|
||||
if (!hasControlRole) return event.followup(getCommonMsg("error.perms.privileged", settings)).awaitSingle()
|
||||
val hasControlRole = permissionService.hasControlRole(settings.guildId, event.interaction.user.id)
|
||||
if (!hasControlRole) return event.createFollowup(getCommonMsg("error.perms.privileged", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
// Check if wizard not started
|
||||
val existingWizard = announcementService.getWizard(settings.guildID, event.interaction.user.id)
|
||||
val existingWizard = announcementService.getWizard(settings.guildId, event.interaction.user.id)
|
||||
?: return event.createFollowup(getMessage("error.wizard.notStarted", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
@@ -389,7 +406,7 @@ class AnnouncementCommand(
|
||||
|
||||
// Confirm guild has access to feature
|
||||
if (!settings.patronGuild) {
|
||||
return event.createFollowup(getCommonMsg("error.patronOnly", settings))
|
||||
return event.createFollowup(getCommonMsg("error.patronOnly", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.withEmbeds(embedService.announcementWizardEmbed(existingWizard, settings))
|
||||
.awaitSingle()
|
||||
@@ -406,11 +423,13 @@ class AnnouncementCommand(
|
||||
|
||||
private suspend fun review(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
// Validate permissions
|
||||
val hasControlRole = event.interaction.member.get().hasControlRole().awaitSingle()
|
||||
if (!hasControlRole) return event.followup(getCommonMsg("error.perms.privileged", settings)).awaitSingle()
|
||||
val hasControlRole = permissionService.hasControlRole(settings.guildId, event.interaction.user.id)
|
||||
if (!hasControlRole) return event.createFollowup(getCommonMsg("error.perms.privileged", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
// Check if wizard not started
|
||||
val existingWizard = announcementService.getWizard(settings.guildID, event.interaction.user.id)
|
||||
val existingWizard = announcementService.getWizard(settings.guildId, event.interaction.user.id)
|
||||
?: return event.createFollowup(getMessage("error.wizard.notStarted", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
@@ -423,11 +442,13 @@ class AnnouncementCommand(
|
||||
|
||||
private suspend fun confirm(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
// Validate permissions
|
||||
val hasControlRole = event.interaction.member.get().hasControlRole().awaitSingle()
|
||||
if (!hasControlRole) return event.followup(getCommonMsg("error.perms.privileged", settings)).awaitSingle()
|
||||
val hasControlRole = permissionService.hasControlRole(settings.guildId, event.interaction.user.id)
|
||||
if (!hasControlRole) return event.createFollowup(getCommonMsg("error.perms.privileged", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
// Check if wizard not started
|
||||
val existingWizard = announcementService.getWizard(settings.guildID, event.interaction.user.id)
|
||||
val existingWizard = announcementService.getWizard(settings.guildId, event.interaction.user.id)
|
||||
?: return event.createFollowup(getMessage("error.wizard.notStarted", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
@@ -451,7 +472,7 @@ class AnnouncementCommand(
|
||||
|
||||
if (existingWizard.editing) announcementService.updateAnnouncement(announcement)
|
||||
else announcementService.createAnnouncement(announcement)
|
||||
announcementService.cancelWizard(settings.guildID, event.interaction.user.id)
|
||||
announcementService.cancelWizard(settings.guildId, event.interaction.user.id)
|
||||
|
||||
val message = if (existingWizard.editing) getMessage("confirm.success.edit", settings)
|
||||
else getMessage("confirm.success.create", settings)
|
||||
@@ -464,10 +485,12 @@ class AnnouncementCommand(
|
||||
|
||||
private suspend fun cancel(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
// Validate permissions
|
||||
val hasControlRole = event.interaction.member.get().hasControlRole().awaitSingle()
|
||||
if (!hasControlRole) return event.followup(getCommonMsg("error.perms.privileged", settings)).awaitSingle()
|
||||
val hasControlRole = permissionService.hasControlRole(settings.guildId, event.interaction.user.id)
|
||||
if (!hasControlRole) return event.createFollowup(getCommonMsg("error.perms.privileged", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
announcementService.cancelWizard(settings.guildID, event.interaction.user.id)
|
||||
announcementService.cancelWizard(settings.guildId, event.interaction.user.id)
|
||||
|
||||
return event.createFollowup(getMessage("cancel.success", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
@@ -481,11 +504,13 @@ class AnnouncementCommand(
|
||||
.get()
|
||||
|
||||
// Validate permissions
|
||||
val hasControlRole = event.interaction.member.get().hasControlRole().awaitSingle()
|
||||
if (!hasControlRole) return event.followup(getCommonMsg("error.perms.privileged", settings)).awaitSingle()
|
||||
val hasControlRole = permissionService.hasControlRole(settings.guildId, event.interaction.user.id)
|
||||
if (!hasControlRole) return event.createFollowup(getCommonMsg("error.perms.privileged", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
// Check if wizard already started
|
||||
val existingWizard = announcementService.getWizard(settings.guildID, event.interaction.user.id)
|
||||
val existingWizard = announcementService.getWizard(settings.guildId, event.interaction.user.id)
|
||||
if (existingWizard != null) {
|
||||
return event.createFollowup(getMessage("error.wizard.started", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
@@ -493,11 +518,13 @@ class AnnouncementCommand(
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
val announcement = announcementService.getAnnouncement(settings.guildID, announcementId)
|
||||
?: return event.followupEphemeral(getCommonMsg("error.notFound.announcement", settings)).awaitSingle()
|
||||
val announcement = announcementService.getAnnouncement(settings.guildId, announcementId)
|
||||
?: return event.createFollowup(getCommonMsg("error.notFound.announcement", settings.locale))
|
||||
.withEphemeral(true)
|
||||
.awaitSingle()
|
||||
|
||||
val newWizard = AnnouncementWizardState(
|
||||
guildId = settings.guildID,
|
||||
guildId = settings.guildId,
|
||||
userId = event.interaction.user.id,
|
||||
editing = true,
|
||||
entity = announcement
|
||||
@@ -517,11 +544,13 @@ class AnnouncementCommand(
|
||||
.get()
|
||||
|
||||
// Validate permissions
|
||||
val hasControlRole = event.interaction.member.get().hasControlRole().awaitSingle()
|
||||
if (!hasControlRole) return event.followup(getCommonMsg("error.perms.privileged", settings)).awaitSingle()
|
||||
val hasControlRole = permissionService.hasControlRole(settings.guildId, event.interaction.user.id)
|
||||
if (!hasControlRole) return event.createFollowup(getCommonMsg("error.perms.privileged", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
// Check if wizard already started
|
||||
val existingWizard = announcementService.getWizard(settings.guildID, event.interaction.user.id)
|
||||
val existingWizard = announcementService.getWizard(settings.guildId, event.interaction.user.id)
|
||||
if (existingWizard != null) {
|
||||
return event.createFollowup(getMessage("error.wizard.started", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
@@ -529,11 +558,13 @@ class AnnouncementCommand(
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
val announcement = announcementService.getAnnouncement(settings.guildID, announcementId)
|
||||
?: return event.followupEphemeral(getCommonMsg("error.notFound.announcement", settings)).awaitSingle()
|
||||
val announcement = announcementService.getAnnouncement(settings.guildId, announcementId)
|
||||
?: return event.createFollowup(getCommonMsg("error.notFound.announcement", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
val newWizard = AnnouncementWizardState(
|
||||
guildId = settings.guildID,
|
||||
guildId = settings.guildId,
|
||||
userId = event.interaction.user.id,
|
||||
editing = false,
|
||||
entity = announcement.copy(id = KeyGenerator.generateAnnouncementId())
|
||||
@@ -553,12 +584,12 @@ class AnnouncementCommand(
|
||||
.get()
|
||||
|
||||
// Validate permissions
|
||||
val hasControlRole = event.interaction.member.get().hasControlRole().awaitSingle()
|
||||
if (!hasControlRole) return event.followup(getCommonMsg("error.perms.privileged", settings)).awaitSingle()
|
||||
val hasControlRole = permissionService.hasControlRole(settings.guildId, event.interaction.user.id)
|
||||
if (!hasControlRole) return event.createFollowup(getCommonMsg("error.perms.privileged", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
// If announcement is being edited, cancel the editor
|
||||
announcementService.cancelWizard(settings.guildID, announcementId)
|
||||
announcementService.deleteAnnouncement(settings.guildID, announcementId)
|
||||
announcementService.deleteAnnouncement(settings.guildId, announcementId)
|
||||
|
||||
return event.createFollowup(getMessage("delete.success", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
@@ -576,11 +607,15 @@ class AnnouncementCommand(
|
||||
.get()
|
||||
|
||||
// Validate permissions
|
||||
val hasControlRole = event.interaction.member.get().hasControlRole().awaitSingle()
|
||||
if (!hasControlRole) return event.followup(getCommonMsg("error.perms.privileged", settings)).awaitSingle()
|
||||
val hasControlRole = permissionService.hasControlRole(settings.guildId, event.interaction.user.id)
|
||||
if (!hasControlRole) return event.createFollowup(getCommonMsg("error.perms.privileged", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
val announcement = announcementService.getAnnouncement(settings.guildID, announcementId)
|
||||
?: return event.followupEphemeral(getCommonMsg("error.notFound.announcement", settings)).awaitSingle()
|
||||
val announcement = announcementService.getAnnouncement(settings.guildId, announcementId)
|
||||
?: return event.createFollowup(getCommonMsg("error.notFound.announcement", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
val new = announcement.copy(enabled = enabled)
|
||||
announcementService.updateAnnouncement(new)
|
||||
@@ -600,8 +635,10 @@ class AnnouncementCommand(
|
||||
.map(ApplicationCommandInteractionOptionValue::asString)
|
||||
.get()
|
||||
|
||||
val announcement = announcementService.getAnnouncement(settings.guildID, announcementId)
|
||||
?: return event.followupEphemeral(getCommonMsg("error.notFound.announcement", settings)).awaitSingle()
|
||||
val announcement = announcementService.getAnnouncement(settings.guildId, announcementId)
|
||||
?: return event.createFollowup(getCommonMsg("error.notFound.announcement", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
return event.createFollowup()
|
||||
.withEphemeral(ephemeral)
|
||||
@@ -633,11 +670,13 @@ class AnnouncementCommand(
|
||||
.getOrNull()
|
||||
|
||||
// Get filtered announcements
|
||||
val announcements = announcementService.getAllAnnouncements(settings.guildID, type, showDisabled)
|
||||
val announcements = announcementService.getAllAnnouncements(settings.guildId, type, showDisabled)
|
||||
.filter { it.calendarNumber == calendar }
|
||||
|
||||
return if (announcements.isEmpty()) {
|
||||
event.followupEphemeral(getMessage("list.success.none", settings)).awaitSingle()
|
||||
event.createFollowup(getMessage("list.success.none", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
} else if (announcements.size == 1) {
|
||||
event.createFollowup()
|
||||
.withEphemeral(ephemeral)
|
||||
@@ -648,10 +687,15 @@ class AnnouncementCommand(
|
||||
} else {
|
||||
val limit = if (amount > 0) amount.coerceAtMost(announcements.size) else announcements.size
|
||||
|
||||
val message = event.followupEphemeral(getMessage("list.success.many", settings, "$limit")).awaitSingle()
|
||||
val message = event.createFollowup(getMessage("list.success.many", settings, "$limit"))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
announcements.subList(0, limit).forEach { announcement ->
|
||||
event.followupEphemeral(embedService.condensedAnnouncementEmbed(announcement, settings)).awaitSingle()
|
||||
event.createFollowup()
|
||||
.withEmbeds(embedService.condensedAnnouncementEmbed(announcement, settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
message
|
||||
@@ -674,8 +718,10 @@ class AnnouncementCommand(
|
||||
.map(ApplicationCommandInteractionOptionValue::asSnowflake)
|
||||
.getOrNull()
|
||||
|
||||
val announcement = announcementService.getAnnouncement(settings.guildID, announcementId)
|
||||
?: return event.followupEphemeral(getCommonMsg("error.notFound.announcement", settings)).awaitSingle()
|
||||
val announcement = announcementService.getAnnouncement(settings.guildId, announcementId)
|
||||
?: return event.createFollowup(getCommonMsg("error.notFound.announcement", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
var newSubs = announcement.subscribers
|
||||
if (userId != null) newSubs = newSubs.copy(users = newSubs.users + userId)
|
||||
@@ -707,8 +753,10 @@ class AnnouncementCommand(
|
||||
.map(ApplicationCommandInteractionOptionValue::asSnowflake)
|
||||
.getOrNull()
|
||||
|
||||
val announcement = announcementService.getAnnouncement(settings.guildID, announcementId)
|
||||
?: return event.followupEphemeral(getCommonMsg("error.notFound.announcement", settings)).awaitSingle()
|
||||
val announcement = announcementService.getAnnouncement(settings.guildId, announcementId)
|
||||
?: return event.createFollowup(getCommonMsg("error.notFound.announcement", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
var newSubs = announcement.subscribers
|
||||
if (userId != null) newSubs = newSubs.copy(users = newSubs.users - userId)
|
||||
|
||||
@@ -3,39 +3,39 @@ 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.Guild
|
||||
import discord4j.core.`object`.entity.Member
|
||||
import discord4j.core.`object`.entity.Message
|
||||
import kotlinx.coroutines.reactor.mono
|
||||
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.business.StaticMessageService
|
||||
import org.dreamexposure.discal.core.entities.response.UpdateCalendarResponse
|
||||
import org.dreamexposure.discal.core.enums.calendar.CalendarHost
|
||||
import org.dreamexposure.discal.core.extensions.discord4j.*
|
||||
import org.dreamexposure.discal.core.extensions.isValidTimezone
|
||||
import org.dreamexposure.discal.core.business.CalendarService
|
||||
import org.dreamexposure.discal.core.business.EmbedService
|
||||
import org.dreamexposure.discal.core.business.PermissionService
|
||||
import org.dreamexposure.discal.core.config.Config
|
||||
import org.dreamexposure.discal.core.extensions.toZoneId
|
||||
import org.dreamexposure.discal.core.logger.LOGGER
|
||||
import org.dreamexposure.discal.core.`object`.GuildSettings
|
||||
import org.dreamexposure.discal.core.`object`.Wizard
|
||||
import org.dreamexposure.discal.core.`object`.calendar.PreCalendar
|
||||
import org.dreamexposure.discal.core.`object`.new.Calendar
|
||||
import org.dreamexposure.discal.core.`object`.new.CalendarMetadata
|
||||
import org.dreamexposure.discal.core.`object`.new.CalendarWizardState
|
||||
import org.dreamexposure.discal.core.`object`.new.GuildSettings
|
||||
import org.dreamexposure.discal.core.utils.getCommonMsg
|
||||
import org.springframework.stereotype.Component
|
||||
import reactor.core.publisher.Mono
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
|
||||
@Component
|
||||
class CalendarCommand(
|
||||
private val wizard: Wizard<PreCalendar>,
|
||||
private val staticMessageService: StaticMessageService
|
||||
private val calendarService: CalendarService,
|
||||
private val permissionService: PermissionService,
|
||||
private val embedService: EmbedService,
|
||||
) : SlashCommand {
|
||||
override val name = "calendar"
|
||||
override val hasSubcommands = true
|
||||
override val ephemeral = true
|
||||
private val OVERVIEW_EVENT_COUNT = Config.CALENDAR_OVERVIEW_DEFAULT_EVENT_COUNT.getInt()
|
||||
|
||||
@Deprecated("Use new handleSuspend for K-coroutines")
|
||||
override fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
override suspend fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
return when (event.options[0].name) {
|
||||
"view" -> view(event, settings)
|
||||
"list" -> list(event, settings)
|
||||
"create" -> create(event, settings)
|
||||
"name" -> name(event, settings)
|
||||
"description" -> description(event, settings)
|
||||
@@ -45,232 +45,373 @@ class CalendarCommand(
|
||||
"cancel" -> cancel(event, settings)
|
||||
"delete" -> delete(event, settings)
|
||||
"edit" -> edit(event, settings)
|
||||
else -> Mono.empty() //Never can reach this, makes compiler happy.
|
||||
else -> throw IllegalStateException("Invalid subcommand specified")
|
||||
}
|
||||
}
|
||||
|
||||
private fun create(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
private suspend fun view(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
val showOverview = event.options[0].getOption("overview")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asBoolean)
|
||||
.orElse(true)
|
||||
val calendarNumber = event.options[0].getOption("calendar")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asLong)
|
||||
.map(Long::toInt)
|
||||
.orElse(1)
|
||||
|
||||
val calendar = calendarService.getCalendar(settings.guildId, calendarNumber)
|
||||
if (calendar == null) {
|
||||
return event.createFollowup(getCommonMsg("error.notFound.calendar", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
val events = if (showOverview)
|
||||
calendarService.getUpcomingEvents(settings.guildId, calendarNumber, OVERVIEW_EVENT_COUNT)
|
||||
else null
|
||||
|
||||
return event.createFollowup()
|
||||
.withEmbeds(embedService.linkCalendarEmbed(calendar, events))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
private suspend fun list(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
val calendars = calendarService.getAllCalendars(settings.guildId)
|
||||
|
||||
if (calendars.isEmpty()) {
|
||||
return event.createFollowup(getMessage("list.success.none", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
} else if (calendars.size == 1) {
|
||||
return event.createFollowup(getMessage("list.success.one", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.withEmbeds(embedService.linkCalendarEmbed(calendars[0]))
|
||||
.awaitSingle()
|
||||
} else {
|
||||
val response = event.createFollowup(getMessage("list.success.many", settings, "${calendars.size}"))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
calendars.forEach {
|
||||
event.createFollowup()
|
||||
.withEmbeds(embedService.linkCalendarEmbed(it))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun create(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
val name = event.options[0].getOption("name")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asString)
|
||||
.get()
|
||||
|
||||
val description = event.options[0].getOption("description")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asString)
|
||||
.orElse("")
|
||||
|
||||
val timezone = event.options[0].getOption("timezone")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asString)
|
||||
.orElse("")
|
||||
|
||||
.orElse("UTC")
|
||||
.toZoneId() ?: ZoneId.of("UTC")
|
||||
val host = event.options[0].getOption("host")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asString)
|
||||
.map(CalendarHost::valueOf)
|
||||
.orElse(CalendarHost.GOOGLE)
|
||||
.map(CalendarMetadata.Host::valueOf)
|
||||
.orElse(CalendarMetadata.Host.GOOGLE)
|
||||
|
||||
return Mono.justOrEmpty(event.interaction.member).filterWhen(Member::hasElevatedPermissions).flatMap {
|
||||
if (wizard.get(settings.guildID) == null) {
|
||||
//Start calendar wizard
|
||||
val pre = PreCalendar.new(settings.guildID, host, name)
|
||||
pre.description = description
|
||||
pre.timezone = timezone.toZoneId() // Extension method auto-checks if the timezone is valid
|
||||
// Validate permissions
|
||||
val hasElevatedPerms = permissionService.hasElevatedPermissions(settings.guildId, event.interaction.user.id)
|
||||
if (!hasElevatedPerms) return event.createFollowup(getCommonMsg("error.perms.elevated", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
// Message content for wizard start so that if a bad optional timezone is given, we can provide better feedback.
|
||||
val msg = if (timezone.isNotEmpty() && !timezone.isValidTimezone()) {
|
||||
// Invalid timezone specified
|
||||
getMessage("create.success.badTimezone", settings)
|
||||
} else {
|
||||
getMessage("create.success", settings)
|
||||
}
|
||||
// Check if wizard already started
|
||||
val existingWizard = calendarService.getCalendarWizard(settings.guildId, event.interaction.user.id)
|
||||
if (existingWizard != null) return event.createFollowup(getMessage("error.wizard.started", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.withEmbeds(embedService.calendarWizardEmbed(existingWizard, settings))
|
||||
.awaitSingle()
|
||||
|
||||
event.interaction.guild
|
||||
.filterWhen(Guild::canAddCalendar)
|
||||
.doOnNext { wizard.start(pre) } //only start wizard if another calendar can be added
|
||||
.map { CalendarEmbed.pre(it, settings, pre) }
|
||||
.flatMap { event.followupEphemeral(msg, it) }
|
||||
.switchIfEmpty(event.followupEphemeral(getCommonMsg("error.calendar.max", settings)))
|
||||
} else {
|
||||
event.interaction.guild
|
||||
.map { CalendarEmbed.pre(it, settings, wizard.get(settings.guildID)!!) }
|
||||
.flatMap { event.followupEphemeral(getMessage("error.wizard.started", settings), it) }
|
||||
}
|
||||
}.switchIfEmpty(event.followupEphemeral(getCommonMsg("error.perms.elevated", settings)))
|
||||
// Check if new calendar can be added
|
||||
if (!calendarService.canAddNewCalendar(settings.guildId))
|
||||
return event.createFollowup(getCommonMsg("error.calendar.max", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
val newWizard = CalendarWizardState(
|
||||
guildId = settings.guildId,
|
||||
userId = event.interaction.user.id,
|
||||
editing = false,
|
||||
entity = Calendar(
|
||||
metadata = CalendarMetadata(
|
||||
guildId = settings.guildId,
|
||||
number = calendarService.getNextCalendarNumber(settings.guildId),
|
||||
host = host,
|
||||
id = "NOT_YET_GENERATED",
|
||||
address = "NOT_YET_GENERATED",
|
||||
external = false,
|
||||
secrets = CalendarMetadata.Secrets(
|
||||
credentialId = 0,
|
||||
privateKey = "NOT_YET_GENERATED",
|
||||
expiresAt = Instant.now(),
|
||||
refreshToken = "NOT_YET_GENERATED",
|
||||
accessToken = "NOT_YET_GENERATED",
|
||||
),
|
||||
),
|
||||
name = name,
|
||||
description = description,
|
||||
timezone = timezone,
|
||||
hostLink = "NOT_YET_GENERATED",
|
||||
)
|
||||
)
|
||||
calendarService.putCalendarWizard(newWizard)
|
||||
|
||||
return event.createFollowup(getMessage("create.success", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.withEmbeds(embedService.calendarWizardEmbed(newWizard, settings))
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
private fun name(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
private suspend fun name(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
val name = event.options[0].getOption("name")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asString)
|
||||
.get()
|
||||
|
||||
return Mono.justOrEmpty(event.interaction.member).filterWhen(Member::hasElevatedPermissions).flatMap {
|
||||
val pre = wizard.get(settings.guildID)
|
||||
if (pre != null) {
|
||||
pre.name = name
|
||||
event.interaction.guild
|
||||
.map { CalendarEmbed.pre(it, settings, pre) }
|
||||
.flatMap { event.followupEphemeral(getMessage("name.success", settings), it) }
|
||||
} else {
|
||||
event.followupEphemeral(getMessage("error.wizard.notStarted", settings))
|
||||
}
|
||||
}.switchIfEmpty(event.followupEphemeral(getCommonMsg("error.perms.elevated", settings)))
|
||||
// Validate permissions
|
||||
val hasElevatedPerms = permissionService.hasElevatedPermissions(settings.guildId, event.interaction.user.id)
|
||||
if (!hasElevatedPerms) return event.createFollowup(getCommonMsg("error.perms.elevated", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
// Check if wizard not started
|
||||
val existingWizard = calendarService.getCalendarWizard(settings.guildId, event.interaction.user.id)
|
||||
if (existingWizard == null) return event.createFollowup(getMessage("error.wizard.notStarted", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
val alteredWizard = existingWizard.copy(entity = existingWizard.entity.copy(name = name))
|
||||
calendarService.putCalendarWizard(alteredWizard)
|
||||
|
||||
|
||||
return event.createFollowup(getMessage("name.success", settings))
|
||||
.withEmbeds(embedService.calendarWizardEmbed(alteredWizard, settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
private fun description(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
private suspend fun description(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
val desc = event.options[0].getOption("description")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asString)
|
||||
.get()
|
||||
|
||||
return Mono.justOrEmpty(event.interaction.member).filterWhen(Member::hasElevatedPermissions).flatMap {
|
||||
val pre = wizard.get(settings.guildID)
|
||||
if (pre != null) {
|
||||
pre.description = desc
|
||||
event.interaction.guild
|
||||
.map { CalendarEmbed.pre(it, settings, pre) }
|
||||
.flatMap { event.followupEphemeral(getMessage("description.success", settings), it) }
|
||||
} else {
|
||||
event.followupEphemeral(getMessage("error.wizard.notStarted", settings))
|
||||
}
|
||||
}.switchIfEmpty(event.followupEphemeral(getCommonMsg("error.perms.elevated", settings)))
|
||||
// Validate permissions
|
||||
val hasElevatedPerms = permissionService.hasElevatedPermissions(settings.guildId, event.interaction.user.id)
|
||||
if (!hasElevatedPerms) return event.createFollowup(getCommonMsg("error.perms.elevated", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
// Check if wizard not started
|
||||
val existingWizard = calendarService.getCalendarWizard(settings.guildId, event.interaction.user.id)
|
||||
if (existingWizard == null) return event.createFollowup(getMessage("error.wizard.notStarted", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
val alteredWizard = existingWizard.copy(entity = existingWizard.entity.copy(description = desc))
|
||||
calendarService.putCalendarWizard(alteredWizard)
|
||||
|
||||
return event.createFollowup(getMessage("description.success", settings))
|
||||
.withEmbeds(embedService.calendarWizardEmbed(alteredWizard, settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
private fun timezone(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
private suspend fun timezone(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
val timezone = event.options[0].getOption("timezone")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asString)
|
||||
.get()
|
||||
.get().toZoneId()
|
||||
|
||||
return Mono.justOrEmpty(event.interaction.member).filterWhen(Member::hasElevatedPermissions).flatMap {
|
||||
val pre = wizard.get(settings.guildID)
|
||||
if (pre != null) {
|
||||
if (timezone.isValidTimezone()) {
|
||||
pre.timezone = ZoneId.of(timezone)
|
||||
// Validate permissions
|
||||
val hasElevatedPerms = permissionService.hasElevatedPermissions(settings.guildId, event.interaction.user.id)
|
||||
if (!hasElevatedPerms) return event.createFollowup(getCommonMsg("error.perms.elevated", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
event.interaction.guild
|
||||
.map { CalendarEmbed.pre(it, settings, pre) }
|
||||
.flatMap { event.followupEphemeral(getMessage("timezone.success", settings), it) }
|
||||
} else {
|
||||
event.followupEphemeral(getMessage("timezone.failure.invalid", settings))
|
||||
}
|
||||
} else {
|
||||
event.followupEphemeral(getMessage("error.wizard.notStarted", settings))
|
||||
}
|
||||
}.switchIfEmpty(event.followupEphemeral(getCommonMsg("error.perms.elevated", settings)))
|
||||
// Check if wizard not started
|
||||
val existingWizard = calendarService.getCalendarWizard(settings.guildId, event.interaction.user.id)
|
||||
if (existingWizard == null) return event.createFollowup(getMessage("error.wizard.notStarted", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
// Timezone will be null if not a valid tz
|
||||
if (timezone == null) return event.createFollowup(getMessage("timezone.failure.invalid", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.withEmbeds(embedService.calendarWizardEmbed(existingWizard, settings))
|
||||
.awaitSingle()
|
||||
|
||||
|
||||
val alteredWizard = existingWizard.copy(entity = existingWizard.entity.copy(timezone = timezone))
|
||||
calendarService.putCalendarWizard(alteredWizard)
|
||||
|
||||
return event.createFollowup(getMessage("timezone.success", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.withEmbeds(embedService.calendarWizardEmbed(alteredWizard, settings))
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
private fun review(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
return Mono.justOrEmpty(event.interaction.member).filterWhen(Member::hasElevatedPermissions).flatMap {
|
||||
val pre = wizard.get(settings.guildID)
|
||||
if (pre != null) {
|
||||
event.interaction.guild.flatMap {
|
||||
event.followupEphemeral(CalendarEmbed.pre(it, settings, pre))
|
||||
}
|
||||
} else {
|
||||
event.followupEphemeral(getMessage("error.wizard.notStarted", settings))
|
||||
}
|
||||
}.switchIfEmpty(event.followupEphemeral(getCommonMsg("error.perms.elevated", settings)))
|
||||
private suspend fun review(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
// Validate permissions
|
||||
val hasElevatedPerms = permissionService.hasElevatedPermissions(settings.guildId, event.interaction.user.id)
|
||||
if (!hasElevatedPerms) return event.createFollowup(getCommonMsg("error.perms.elevated", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
// Check if wizard not started
|
||||
val existingWizard = calendarService.getCalendarWizard(settings.guildId, event.interaction.user.id)
|
||||
if (existingWizard == null) return event.createFollowup(getMessage("error.wizard.notStarted", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
return event.createFollowup()
|
||||
.withEmbeds(embedService.calendarWizardEmbed(existingWizard, settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
private fun confirm(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
return Mono.justOrEmpty(event.interaction.member).filterWhen(Member::hasElevatedPermissions).flatMap {
|
||||
val pre = wizard.get(settings.guildID)
|
||||
if (pre != null) {
|
||||
if (!pre.hasRequiredValues()) {
|
||||
return@flatMap event.followupEphemeral(getMessage("confirm.failure.missing", settings))
|
||||
}
|
||||
private suspend fun confirm(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
// Validate permissions
|
||||
val hasElevatedPerms = permissionService.hasElevatedPermissions(settings.guildId, event.interaction.user.id)
|
||||
if (!hasElevatedPerms) return event.createFollowup(getCommonMsg("error.perms.elevated", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
event.interaction.guild.flatMap { guild ->
|
||||
if (!pre.editing) {
|
||||
// New calendar
|
||||
pre.createSpec(guild)
|
||||
.flatMap(guild::createCalendar)
|
||||
.doOnNext { wizard.remove(settings.guildID) }
|
||||
.flatMap {
|
||||
event.followupEphemeral(
|
||||
getMessage("confirm.success.create", settings),
|
||||
CalendarEmbed.link(guild, settings, it)
|
||||
)
|
||||
}.doOnError {
|
||||
LOGGER.error("Create calendar with command failure", it)
|
||||
}.onErrorResume {
|
||||
event.followupEphemeral(getMessage("confirm.failure.create", settings))
|
||||
}
|
||||
} else {
|
||||
// Editing
|
||||
pre.calendar!!.update(pre.updateSpec())
|
||||
.filter(UpdateCalendarResponse::success)
|
||||
.doOnNext { wizard.remove(settings.guildID) }
|
||||
.flatMap { ucr ->
|
||||
val updateMessages = mono {
|
||||
staticMessageService.updateStaticMessages(settings.guildID, ucr.new!!.calendarNumber)
|
||||
}
|
||||
event.followupEphemeral(
|
||||
getMessage("confirm.success.edit", settings),
|
||||
CalendarEmbed.link(guild, settings, ucr.new!!)
|
||||
).flatMap { updateMessages.thenReturn(it) }
|
||||
}.switchIfEmpty(event.followupEphemeral(getMessage("confirm.failure.edit", settings)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
event.followupEphemeral(getMessage("error.wizard.notStarted", settings))
|
||||
}
|
||||
}.switchIfEmpty(event.followupEphemeral(getCommonMsg("error.perms.elevated", settings)))
|
||||
// Check if wizard not started
|
||||
val existingWizard = calendarService.getCalendarWizard(settings.guildId, event.interaction.user.id)
|
||||
if (existingWizard == null) return event.createFollowup(getMessage("error.wizard.notStarted", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
// Would add checks for required values here, but I think I've basically hand-waved that away now
|
||||
try {
|
||||
val calendar = if (existingWizard.editing) calendarService.updateCalendar(
|
||||
settings.guildId,
|
||||
existingWizard.entity.metadata.number,
|
||||
Calendar.UpdateSpec(
|
||||
name = existingWizard.entity.name,
|
||||
description = existingWizard.entity.description,
|
||||
timezone = existingWizard.entity.timezone,
|
||||
)
|
||||
) else calendarService.createCalendar(
|
||||
settings.guildId,
|
||||
Calendar.CreateSpec(
|
||||
host = existingWizard.entity.metadata.host,
|
||||
number = existingWizard.entity.metadata.number,
|
||||
name = existingWizard.entity.name,
|
||||
description = existingWizard.entity.description,
|
||||
timezone = existingWizard.entity.timezone,
|
||||
)
|
||||
)
|
||||
calendarService.cancelCalendarWizard(settings.guildId, calendar.metadata.number)
|
||||
|
||||
val message = if (existingWizard.editing) getMessage("confirm.success.edit", settings)
|
||||
else getMessage("confirm.success.create", settings)
|
||||
|
||||
return event.createFollowup(message)
|
||||
.withEmbeds(embedService.linkCalendarEmbed(calendar))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
} catch (ex: Exception) {
|
||||
LOGGER.error("Failed to create calendar via command interaction", ex)
|
||||
|
||||
val message = if (existingWizard.editing) getMessage("confirm.failure.edit", settings)
|
||||
else getMessage("confirm.failure.create", settings)
|
||||
|
||||
return event.createFollowup(message)
|
||||
.withEmbeds(embedService.calendarWizardEmbed(existingWizard, settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancel(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
return Mono.justOrEmpty(event.interaction.member).filterWhen(Member::hasElevatedPermissions).flatMap {
|
||||
wizard.remove(settings.guildID)
|
||||
private suspend fun cancel(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
// Validate permissions
|
||||
val hasElevatedPerms = permissionService.hasElevatedPermissions(settings.guildId, event.interaction.user.id)
|
||||
if (!hasElevatedPerms) return event.createFollowup(getCommonMsg("error.perms.elevated", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
event.followupEphemeral(getMessage("cancel.success", settings))
|
||||
}.switchIfEmpty(event.followupEphemeral(getCommonMsg("error.perms.elevated", settings)))
|
||||
calendarService.cancelCalendarWizard(settings.guildId, event.interaction.user.id)
|
||||
|
||||
return event.createFollowup(getMessage("cancel.success", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
private fun delete(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
private suspend fun delete(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
val calendarNumber = event.options[0].getOption("calendar")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asLong)
|
||||
.map(Long::toInt)
|
||||
.orElse(1)
|
||||
|
||||
return Mono.justOrEmpty(event.interaction.member).filterWhen(Member::hasElevatedPermissions).flatMap {
|
||||
// Before we delete the calendar, if the wizard is editing that calendar we need to cancel the wizard
|
||||
val pre = wizard.get(settings.guildID)
|
||||
if (pre != null && pre.calendar?.calendarNumber == calendarNumber) wizard.remove(settings.guildID)
|
||||
// Validate permissions
|
||||
val hasElevatedPerms = permissionService.hasElevatedPermissions(settings.guildId, event.interaction.user.id)
|
||||
if (!hasElevatedPerms) return event.createFollowup(getCommonMsg("error.perms.elevated", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
event.interaction.guild
|
||||
.flatMap { it.getCalendar(calendarNumber) }
|
||||
.flatMap { it.delete() }
|
||||
.flatMap { event.followupEphemeral(getMessage("delete.success", settings)) }
|
||||
.switchIfEmpty(event.followupEphemeral(getCommonMsg("error.notFound.calendar", settings)))
|
||||
}.switchIfEmpty(event.followupEphemeral(getCommonMsg("error.perms.elevated", settings)))
|
||||
calendarService.deleteCalendar(settings.guildId, calendarNumber)
|
||||
|
||||
return event.createFollowup(getMessage("delete.success", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
private fun edit(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
private suspend fun edit(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
val calendarNumber = event.options[0].getOption("calendar")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asLong)
|
||||
.map(Long::toInt)
|
||||
.orElse(1)
|
||||
|
||||
return Mono.justOrEmpty(event.interaction.member).filterWhen(Member::hasElevatedPermissions).flatMap {
|
||||
if (wizard.get(settings.guildID) == null) {
|
||||
event.interaction.guild.flatMap { guild ->
|
||||
guild.getCalendar(calendarNumber)
|
||||
.map { PreCalendar.edit(it) }
|
||||
.doOnNext { wizard.start(it) }
|
||||
.map { CalendarEmbed.pre(guild, settings, it) }
|
||||
.flatMap { event.followupEphemeral(getMessage("edit.success", settings), it) }
|
||||
.switchIfEmpty(event.followupEphemeral(getCommonMsg("error.notFound.calendar", settings)))
|
||||
}
|
||||
} else {
|
||||
event.interaction.guild
|
||||
.map { CalendarEmbed.pre(it, settings, wizard.get(settings.guildID)!!) }
|
||||
.flatMap { event.followupEphemeral(getMessage("error.wizard.started", settings), it) }
|
||||
}
|
||||
}.switchIfEmpty(event.followupEphemeral(getCommonMsg("error.perms.elevated", settings)))
|
||||
// Validate permissions
|
||||
val hasElevatedPerms = permissionService.hasElevatedPermissions(settings.guildId, event.interaction.user.id)
|
||||
if (!hasElevatedPerms) return event.createFollowup(getCommonMsg("error.perms.elevated", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
// Check if wizard already started
|
||||
val existingWizard = calendarService.getCalendarWizard(settings.guildId, event.interaction.user.id)
|
||||
if (existingWizard != null) return event.createFollowup(getMessage("error.wizard.started", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.withEmbeds(embedService.calendarWizardEmbed(existingWizard, settings))
|
||||
.awaitSingle()
|
||||
|
||||
val calendar = calendarService.getCalendar(settings.guildId, calendarNumber)
|
||||
if (calendar == null) return event.createFollowup(getCommonMsg("error.notFound.calendar", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
val newWizard = CalendarWizardState(
|
||||
guildId = settings.guildId,
|
||||
userId = event.interaction.user.id,
|
||||
editing = true,
|
||||
entity = calendar
|
||||
)
|
||||
calendarService.putCalendarWizard(newWizard)
|
||||
|
||||
return event.createFollowup(getMessage("edit.success", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.withEmbeds(embedService.calendarWizardEmbed(newWizard, settings))
|
||||
.awaitSingle()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@ import org.dreamexposure.discal.client.commands.SlashCommand
|
||||
import org.dreamexposure.discal.core.business.AnnouncementService
|
||||
import org.dreamexposure.discal.core.business.CalendarService
|
||||
import org.dreamexposure.discal.core.business.EmbedService
|
||||
import org.dreamexposure.discal.core.extensions.discord4j.followup
|
||||
import org.dreamexposure.discal.core.`object`.GuildSettings
|
||||
import org.dreamexposure.discal.core.`object`.new.GuildSettings
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
@@ -21,12 +20,16 @@ class DiscalCommand(
|
||||
override val hasSubcommands = false
|
||||
override val ephemeral = false
|
||||
|
||||
override suspend fun suspendHandle(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
override suspend fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
val announcementCount = announcementService.getAnnouncementCount()
|
||||
val calendarCount = calendarService.getCalendarCount()
|
||||
val guildCount = event.client.guilds.count().awaitSingle()
|
||||
|
||||
val embed = embedService.discalInfoEmbed(settings, calendarCount, announcementCount)
|
||||
val embed = embedService.discalInfoEmbed(settings, guildCount, calendarCount, announcementCount)
|
||||
|
||||
return event.followup(embed).awaitSingle()
|
||||
return event.createFollowup()
|
||||
.withEmbeds(embed)
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,26 +5,26 @@ import discord4j.core.`object`.command.ApplicationCommandInteractionOption
|
||||
import discord4j.core.`object`.command.ApplicationCommandInteractionOptionValue
|
||||
import discord4j.core.`object`.entity.Message
|
||||
import kotlinx.coroutines.reactor.awaitSingle
|
||||
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
||||
import org.dreamexposure.discal.client.commands.SlashCommand
|
||||
import org.dreamexposure.discal.core.business.CalendarService
|
||||
import org.dreamexposure.discal.core.business.PermissionService
|
||||
import org.dreamexposure.discal.core.business.StaticMessageService
|
||||
import org.dreamexposure.discal.core.extensions.discord4j.followupEphemeral
|
||||
import org.dreamexposure.discal.core.extensions.discord4j.getCalendar
|
||||
import org.dreamexposure.discal.core.extensions.discord4j.hasElevatedPermissions
|
||||
import org.dreamexposure.discal.core.`object`.GuildSettings
|
||||
import org.dreamexposure.discal.core.`object`.new.GuildSettings
|
||||
import org.dreamexposure.discal.core.utils.getCommonMsg
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class DisplayCalendarCommand(
|
||||
private val staticMessageService: StaticMessageService,
|
||||
private val calendarService: CalendarService,
|
||||
private val permissionService: PermissionService,
|
||||
) : SlashCommand {
|
||||
override val name = "displaycal"
|
||||
override val hasSubcommands = true
|
||||
override val ephemeral = true
|
||||
|
||||
|
||||
override suspend fun suspendHandle(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
override suspend fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
return when (event.options[0].name) {
|
||||
"new" -> new(event, settings)
|
||||
else -> throw IllegalStateException("Invalid subcommand specified")
|
||||
@@ -43,18 +43,24 @@ class DisplayCalendarCommand(
|
||||
.orElse(1)
|
||||
|
||||
// Validate control role
|
||||
val hasElevatedPerms = event.interaction.member.get().hasElevatedPermissions().awaitSingle()
|
||||
val hasElevatedPerms = permissionService.hasElevatedPermissions(settings.guildId, event.interaction.user.id)
|
||||
if (!hasElevatedPerms)
|
||||
return event.followupEphemeral(getCommonMsg("error.perms.elevated", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.perms.elevated", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
// Validate calendar exists
|
||||
val calendar = event.interaction.guild.flatMap { it.getCalendar(calendarNumber) }.awaitSingleOrNull()
|
||||
val calendar = calendarService.getCalendar(settings.guildId, calendarNumber)
|
||||
if (calendar == null)
|
||||
return event.followupEphemeral(getCommonMsg("error.notFound.calendar", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.notFound.calendar", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
// Create and respond
|
||||
staticMessageService.createStaticMessage(settings.guildID, event.interaction.channelId, calendarNumber, hour)
|
||||
staticMessageService.createStaticMessage(settings.guildId, event.interaction.channelId, calendarNumber, hour)
|
||||
|
||||
return event.followupEphemeral(getCommonMsg("success.generic", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("success.generic", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,43 +1,42 @@
|
||||
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.reactive.awaitSingle
|
||||
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
|
||||
import org.dreamexposure.discal.core.extensions.discord4j.getCalendar
|
||||
import org.dreamexposure.discal.core.business.CalendarService
|
||||
import org.dreamexposure.discal.core.business.EmbedService
|
||||
import org.dreamexposure.discal.core.`object`.new.GuildSettings
|
||||
import org.dreamexposure.discal.core.utils.getCommonMsg
|
||||
import org.springframework.stereotype.Component
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.function.TupleUtils
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.DateTimeParseException
|
||||
|
||||
@Component
|
||||
class EventsCommand : SlashCommand {
|
||||
class EventsCommand(
|
||||
private val calendarService: CalendarService,
|
||||
private val embedService: EmbedService,
|
||||
) : SlashCommand {
|
||||
override val name = "events"
|
||||
override val hasSubcommands = true
|
||||
override val ephemeral = false
|
||||
|
||||
@Deprecated("Use new handleSuspend for K-coroutines")
|
||||
override fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
|
||||
override suspend fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
return when (event.options[0].name) {
|
||||
"upcoming" -> upcomingEventsSubcommand(event, settings)
|
||||
"ongoing" -> ongoingEventsSubcommand(event, settings)
|
||||
"today" -> eventsTodaySubcommand(event, settings)
|
||||
"range" -> eventsRangeSubcommand(event, settings)
|
||||
else -> Mono.empty() //Never can reach this, makes compiler happy.
|
||||
"upcoming" -> upcomingEvents(event, settings)
|
||||
"ongoing" -> ongoingEvents(event, settings)
|
||||
"today" -> eventsToday(event, settings)
|
||||
"range" -> eventsRange(event, settings)
|
||||
else -> throw IllegalStateException("Invalid subcommand specified")
|
||||
}
|
||||
}
|
||||
|
||||
private fun upcomingEventsSubcommand(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
private suspend fun upcomingEvents(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
val calendarNumber = event.options[0].getOption("calendar")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asLong)
|
||||
@@ -49,156 +48,165 @@ class EventsCommand : SlashCommand {
|
||||
.map(ApplicationCommandInteractionOptionValue::asLong)
|
||||
.map(Long::toInt)
|
||||
.orElse(1)
|
||||
.coerceIn(1, 15)
|
||||
|
||||
val events = calendarService.getUpcomingEvents(settings.guildId, calendarNumber, amount)
|
||||
|
||||
if (amount < 1 || amount > 15) {
|
||||
return event.followup(getMessage("upcoming.failure.outOfRange", settings))
|
||||
}
|
||||
return if (events.isEmpty()) {
|
||||
event.createFollowup(getMessage("upcoming.success.none", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
} else if (events.size == 1) {
|
||||
event.createFollowup(getMessage("upcoming.success.one", settings))
|
||||
.withEmbeds(embedService.fullEventEmbed(events[0], settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
} else {
|
||||
val response = event.createFollowup(getMessage("upcoming.success.many", settings, "${events.size}"))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
return event.interaction.guild.flatMap { guild ->
|
||||
guild.getCalendar(calendarNumber).flatMap { cal ->
|
||||
cal.getUpcomingEvents(amount).collectList().flatMap { events ->
|
||||
if (events.isEmpty()) {
|
||||
event.followup(getMessage("upcoming.success.none", settings))
|
||||
} else if (events.size == 1) {
|
||||
event.followup(
|
||||
getMessage("upcoming.success.one", settings),
|
||||
EventEmbed.getFull(guild, settings, events[0])
|
||||
)
|
||||
} else {
|
||||
event.followup(
|
||||
getMessage("upcoming.success.many", settings, "${events.size}")
|
||||
).flatMapMany {
|
||||
Flux.fromIterable(events)
|
||||
}.concatMap {
|
||||
event.followup(EventEmbed.getCondensed(guild, settings, it))
|
||||
}.last()
|
||||
}
|
||||
}
|
||||
}.switchIfEmpty(event.followup(getCommonMsg("error.notFound.calendar", settings)))
|
||||
}
|
||||
}
|
||||
|
||||
private fun ongoingEventsSubcommand(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
val calendarNumber = event.options[0].getOption("calendar")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asLong)
|
||||
.map(Long::toInt)
|
||||
.orElse(1)
|
||||
|
||||
return event.interaction.guild.flatMap { guild ->
|
||||
guild.getCalendar(calendarNumber).flatMap { cal ->
|
||||
cal.getOngoingEvents().collectList().flatMap { events ->
|
||||
if (events.isEmpty()) {
|
||||
event.followup(getMessage("ongoing.success.none", settings))
|
||||
} else if (events.size == 1) {
|
||||
event.followup(
|
||||
getMessage("ongoing.success.one", settings),
|
||||
EventEmbed.getFull(guild, settings, events[0])
|
||||
)
|
||||
} else {
|
||||
event.followup(
|
||||
getMessage("ongoing.success.many", settings, "${events.size}")
|
||||
).flatMapMany {
|
||||
Flux.fromIterable(events)
|
||||
}.concatMap {
|
||||
event.followup(EventEmbed.getCondensed(guild, settings, it))
|
||||
}.last()
|
||||
}
|
||||
}
|
||||
}.switchIfEmpty(event.followup(getCommonMsg("error.notFound.calendar", settings)))
|
||||
}
|
||||
}
|
||||
|
||||
private fun eventsTodaySubcommand(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
val calendarNumber = event.options[0].getOption("calendar")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asLong)
|
||||
.map(Long::toInt)
|
||||
.orElse(1)
|
||||
|
||||
return event.interaction.guild.flatMap { guild ->
|
||||
guild.getCalendar(calendarNumber).flatMap { cal ->
|
||||
cal.getEventsInNext24HourPeriod(Instant.now()).collectList().flatMap { events ->
|
||||
if (events.isEmpty()) {
|
||||
event.followup(getMessage("today.success.none", settings))
|
||||
} else if (events.size == 1) {
|
||||
event.followup(
|
||||
getMessage("today.success.one", settings),
|
||||
EventEmbed.getFull(guild, settings, events[0])
|
||||
)
|
||||
} else {
|
||||
event.followup(
|
||||
getMessage("today.success.many", settings, "${events.size}")
|
||||
).flatMapMany {
|
||||
Flux.fromIterable(events)
|
||||
}.concatMap {
|
||||
event.followup(EventEmbed.getCondensed(guild, settings, it))
|
||||
}.last()
|
||||
}
|
||||
}
|
||||
}.switchIfEmpty(event.followup(getCommonMsg("error.notFound.calendar", settings)))
|
||||
}
|
||||
}
|
||||
|
||||
private fun eventsRangeSubcommand(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
val gMono = event.interaction.guild.cache()
|
||||
|
||||
val calMono = Mono.justOrEmpty(event.options[0].getOption("calendar").flatMap { it.value })
|
||||
.map { it.asLong().toInt() }
|
||||
.defaultIfEmpty(1)
|
||||
.flatMap { num ->
|
||||
gMono.flatMap {
|
||||
it.getCalendar(num)
|
||||
}
|
||||
}.cache()
|
||||
|
||||
val sMono = Mono.justOrEmpty(event.options[0].getOption("start").flatMap { it.value })
|
||||
.map { it.asString() }
|
||||
.flatMap { value ->
|
||||
calMono.map {
|
||||
val formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd")
|
||||
|
||||
LocalDate.parse(value, formatter).atStartOfDay(it.timezone)
|
||||
}
|
||||
}.map(ZonedDateTime::toInstant)
|
||||
|
||||
val eMono = Mono.justOrEmpty(event.options[0].getOption("end").flatMap { it.value })
|
||||
.map { it.asString() }
|
||||
.flatMap { value ->
|
||||
calMono.map {
|
||||
val formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd")
|
||||
|
||||
//At end of day
|
||||
LocalDate.parse(value, formatter).plusDays(1).atStartOfDay(it.timezone)
|
||||
}
|
||||
}.map(ZonedDateTime::toInstant)
|
||||
|
||||
return Mono.zip(gMono, calMono, sMono, eMono).flatMap(
|
||||
TupleUtils.function { guild, cal, start, end ->
|
||||
cal.getEventsInTimeRange(start, end).collectList().flatMap { events ->
|
||||
if (events.isEmpty()) {
|
||||
event.followup(getMessage("range.success.none", settings))
|
||||
} else if (events.size == 1) {
|
||||
event.followup(
|
||||
getMessage("range.success.one", settings),
|
||||
EventEmbed.getFull(guild, settings, events[0])
|
||||
)
|
||||
} else if (events.size > 15) {
|
||||
event.followup(getMessage("range.success.tooMany", settings, "${events.size}", cal.link))
|
||||
} else {
|
||||
event.followup(
|
||||
getMessage("range.success.many", settings, "${events.size}")
|
||||
).flatMapMany {
|
||||
Flux.fromIterable(events)
|
||||
}.concatMap {
|
||||
event.followup(EventEmbed.getCondensed(guild, settings, it))
|
||||
}.last()
|
||||
}
|
||||
}
|
||||
}).switchIfEmpty(event.followup(getCommonMsg("error.notFound.calendar", settings)))
|
||||
.onErrorResume(DateTimeParseException::class.java) {
|
||||
event.followup(getCommonMsg("error.format.date", settings))
|
||||
events.forEach {
|
||||
event.createFollowup()
|
||||
.withEmbeds(embedService.condensedEventEmbed(it, settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ongoingEvents(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
val calendarNumber = event.options[0].getOption("calendar")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asLong)
|
||||
.map(Long::toInt)
|
||||
.orElse(1)
|
||||
|
||||
val events = calendarService.getOngoingEvents(settings.guildId, calendarNumber)
|
||||
|
||||
return if (events.isEmpty()) {
|
||||
event.createFollowup(getMessage("ongoing.success.none", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
} else if (events.size == 1) {
|
||||
event.createFollowup(getMessage("ongoing.success.one", settings))
|
||||
.withEmbeds(embedService.fullEventEmbed(events[0], settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
} else {
|
||||
val response = event.createFollowup(getMessage("ongoing.success.many", settings, "${events.size}"))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
events.forEach {
|
||||
event.createFollowup()
|
||||
.withEmbeds(embedService.condensedEventEmbed(it, settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun eventsToday(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
val calendarNumber = event.options[0].getOption("calendar")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asLong)
|
||||
.map(Long::toInt)
|
||||
.orElse(1)
|
||||
|
||||
val events = calendarService.getEventsInNext24HourPeriod(settings.guildId, calendarNumber, Instant.now())
|
||||
|
||||
return if (events.isEmpty()) {
|
||||
event.createFollowup(getMessage("today.success.none", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
} else if (events.size == 1) {
|
||||
event.createFollowup(getMessage("today.success.one", settings))
|
||||
.withEmbeds(embedService.fullEventEmbed(events[0], settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
} else {
|
||||
val response = event.createFollowup(getMessage("today.success.many", settings, "${events.size}"))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
events.forEach {
|
||||
event.createFollowup()
|
||||
.withEmbeds(embedService.condensedEventEmbed(it, settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun eventsRange(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
val calendarNumber = event.options[0].getOption("calendar")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asLong)
|
||||
.map(Long::toInt)
|
||||
.orElse(1)
|
||||
val startInput = event.options[0].getOption("start")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asString)
|
||||
.get()
|
||||
val endInput = event.options[0].getOption("end")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asString)
|
||||
.get()
|
||||
|
||||
val formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd")
|
||||
|
||||
// In order to parse the inputs with timezone, we need to fetch the calendar
|
||||
val calendar = calendarService.getCalendar(settings.guildId, calendarNumber)
|
||||
if (calendar == null) {
|
||||
return event.createFollowup(getCommonMsg("error.notFound.calendar", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
try {
|
||||
val start = LocalDate.parse(startInput, formatter).atStartOfDay(calendar.timezone).toInstant()
|
||||
val end = LocalDate.parse(endInput, formatter).plusDays(1).atStartOfDay(calendar.timezone).toInstant()
|
||||
|
||||
val events = calendarService.getEventsInTimeRange(settings.guildId, calendarNumber, start, end)
|
||||
|
||||
return if (events.isEmpty()) {
|
||||
event.createFollowup(getMessage("range.success.none", settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
} else if (events.size == 1) {
|
||||
event.createFollowup(getMessage("range.success.one", settings))
|
||||
.withEmbeds(embedService.fullEventEmbed(events[0], settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
} else if (events.size > 15) {
|
||||
event.createFollowup(getMessage("range.success.tooMany", settings, "${events.size}", calendar.link))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
} else {
|
||||
val response = event.createFollowup(getMessage("range.success.many", settings, "${events.size}"))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
events.forEach {
|
||||
event.createFollowup()
|
||||
.withEmbeds(embedService.condensedEventEmbed(it, settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
} catch (_: DateTimeParseException) {
|
||||
return event.createFollowup(getCommonMsg("error.format.date", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,7 @@ import discord4j.core.`object`.entity.Message
|
||||
import kotlinx.coroutines.reactor.awaitSingle
|
||||
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.dreamexposure.discal.core.`object`.new.GuildSettings
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
@@ -15,9 +14,9 @@ class HelpCommand : SlashCommand {
|
||||
override val hasSubcommands = false
|
||||
override val ephemeral = true
|
||||
|
||||
override suspend fun suspendHandle(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
return event.followupEphemeral(
|
||||
override suspend fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
return event.createFollowup(
|
||||
getMessage("error.workInProgress", settings, "${Config.URL_BASE.getString()}/commands")
|
||||
).awaitSingle()
|
||||
).withEphemeral(ephemeral).awaitSingle()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,25 +5,27 @@ import discord4j.core.`object`.command.ApplicationCommandInteractionOption
|
||||
import discord4j.core.`object`.command.ApplicationCommandInteractionOptionValue
|
||||
import discord4j.core.`object`.entity.Message
|
||||
import kotlinx.coroutines.reactor.awaitSingle
|
||||
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
||||
import org.dreamexposure.discal.client.commands.SlashCommand
|
||||
import org.dreamexposure.discal.core.business.CalendarService
|
||||
import org.dreamexposure.discal.core.business.EmbedService
|
||||
import org.dreamexposure.discal.core.extensions.discord4j.followup
|
||||
import org.dreamexposure.discal.core.extensions.discord4j.getCalendar
|
||||
import org.dreamexposure.discal.core.`object`.GuildSettings
|
||||
import org.dreamexposure.discal.core.config.Config
|
||||
import org.dreamexposure.discal.core.`object`.new.GuildSettings
|
||||
import org.dreamexposure.discal.core.utils.getCommonMsg
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class LinkCalendarCommand(
|
||||
private val embedService: EmbedService,
|
||||
private val calendarService: CalendarService,
|
||||
) : SlashCommand {
|
||||
override val name = "linkcal"
|
||||
override val hasSubcommands = false
|
||||
override val ephemeral = false
|
||||
|
||||
private val OVERVIEW_EVENT_COUNT = Config.CALENDAR_OVERVIEW_DEFAULT_EVENT_COUNT.getInt()
|
||||
|
||||
override suspend fun suspendHandle(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
|
||||
override suspend fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
val showOverview = event.getOption("overview")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asBoolean)
|
||||
@@ -34,13 +36,20 @@ class LinkCalendarCommand(
|
||||
.map(Long::toInt)
|
||||
.orElse(1)
|
||||
|
||||
val calendar = event.interaction.guild.flatMap {
|
||||
it.getCalendar(calendarNumber)
|
||||
}.awaitSingleOrNull()
|
||||
val calendar = calendarService.getCalendar(settings.guildId, calendarNumber)
|
||||
if (calendar == null) {
|
||||
return event.followup(getCommonMsg("error.notFound.calendar", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.notFound.calendar", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
return event.followup(embedService.linkCalendarEmbed(calendarNumber, settings, showOverview)).awaitSingle()
|
||||
val events = if (showOverview)
|
||||
calendarService.getUpcomingEvents(settings.guildId, calendarNumber, OVERVIEW_EVENT_COUNT)
|
||||
else null
|
||||
|
||||
return event.createFollowup()
|
||||
.withEmbeds(embedService.linkCalendarEmbed(calendar, events))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,15 +5,12 @@ import discord4j.core.`object`.command.ApplicationCommandInteractionOption
|
||||
import discord4j.core.`object`.command.ApplicationCommandInteractionOptionValue
|
||||
import discord4j.core.`object`.entity.Message
|
||||
import kotlinx.coroutines.reactor.awaitSingle
|
||||
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
||||
import org.dreamexposure.discal.client.commands.SlashCommand
|
||||
import org.dreamexposure.discal.core.business.CalendarService
|
||||
import org.dreamexposure.discal.core.business.EmbedService
|
||||
import org.dreamexposure.discal.core.business.PermissionService
|
||||
import org.dreamexposure.discal.core.business.RsvpService
|
||||
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.discord4j.hasElevatedPermissions
|
||||
import org.dreamexposure.discal.core.`object`.GuildSettings
|
||||
import org.dreamexposure.discal.core.`object`.new.GuildSettings
|
||||
import org.dreamexposure.discal.core.utils.getCommonMsg
|
||||
import org.springframework.stereotype.Component
|
||||
import reactor.core.publisher.Mono
|
||||
@@ -22,13 +19,15 @@ import reactor.core.publisher.Mono
|
||||
class RsvpCommand(
|
||||
private val rsvpService: RsvpService,
|
||||
private val embedService: EmbedService,
|
||||
private val permissionService: PermissionService,
|
||||
private val calendarService: CalendarService,
|
||||
) : SlashCommand {
|
||||
override val name = "rsvp"
|
||||
override val hasSubcommands = true
|
||||
override val ephemeral = true
|
||||
|
||||
|
||||
override suspend fun suspendHandle(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
override suspend fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
return when (event.options[0].name) {
|
||||
"ontime" -> onTime(event, settings)
|
||||
"late" -> late(event, settings)
|
||||
@@ -54,34 +53,39 @@ class RsvpCommand(
|
||||
.get()
|
||||
|
||||
val userId = event.interaction.user.id
|
||||
val guild = event.interaction.guild.awaitSingle()
|
||||
val calendar = guild.getCalendar(calendarNumber).awaitSingleOrNull()
|
||||
val calendarEvent = calendar?.getEvent(eventId)?.awaitSingleOrNull()
|
||||
val calendar = calendarService.getCalendar(settings.guildId, calendarNumber)
|
||||
val calendarEvent = calendarService.getEvent(settings.guildId, calendarNumber, eventId)
|
||||
|
||||
// Validate required conditions
|
||||
if (calendar == null)
|
||||
return event.followupEphemeral(getCommonMsg("error.notFound.calendar", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.notFound.calendar", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
if (calendarEvent == null)
|
||||
return event.followupEphemeral(getCommonMsg("error.notFound.event", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.notFound.event", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
if (calendarEvent.isOver())
|
||||
return event.followupEphemeral(getCommonMsg("error.event.ended", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.event.ended", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
var rsvp = rsvpService.getRsvp(guild.id, eventId)
|
||||
var rsvp = rsvpService.getRsvp(settings.guildId, eventId)
|
||||
|
||||
return if (rsvp.hasRoom(userId)) {
|
||||
rsvp = rsvpService.upsertRsvp(rsvp.copyWithUserStatus(userId, goingOnTime = rsvp.goingOnTime + userId))
|
||||
|
||||
event.followupEphemeral(
|
||||
getMessage("onTime.success", settings),
|
||||
embedService.rsvpListEmbed(calendarEvent, rsvp, settings)
|
||||
).awaitSingle()
|
||||
event.createFollowup(getMessage("onTime.success", settings))
|
||||
.withEmbeds(embedService.rsvpListEmbed(calendarEvent, rsvp, settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
} else {
|
||||
rsvp = rsvpService.upsertRsvp(rsvp.copyWithUserStatus(userId, waitlist = rsvp.waitlist + userId))
|
||||
|
||||
event.followupEphemeral(
|
||||
getMessage("onTime.failure.limit", settings),
|
||||
embedService.rsvpListEmbed(calendarEvent, rsvp, settings)
|
||||
).awaitSingle()
|
||||
event.createFollowup(getMessage("onTime.failure.limit", settings))
|
||||
.withEmbeds(embedService.rsvpListEmbed(calendarEvent, rsvp, settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,34 +101,39 @@ class RsvpCommand(
|
||||
.get()
|
||||
|
||||
val userId = event.interaction.user.id
|
||||
val guild = event.interaction.guild.awaitSingle()
|
||||
val calendar = guild.getCalendar(calendarNumber).awaitSingleOrNull()
|
||||
val calendarEvent = calendar?.getEvent(eventId)?.awaitSingleOrNull()
|
||||
val calendar = calendarService.getCalendar(settings.guildId, calendarNumber)
|
||||
val calendarEvent = calendarService.getEvent(settings.guildId, calendarNumber, eventId)
|
||||
|
||||
// Validate required conditions
|
||||
if (calendar == null)
|
||||
return event.followupEphemeral(getCommonMsg("error.notFound.calendar", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.notFound.calendar", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
if (calendarEvent == null)
|
||||
return event.followupEphemeral(getCommonMsg("error.notFound.event", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.notFound.event", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
if (calendarEvent.isOver())
|
||||
return event.followupEphemeral(getCommonMsg("error.event.ended", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.event.ended", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
var rsvp = rsvpService.getRsvp(guild.id, eventId)
|
||||
var rsvp = rsvpService.getRsvp(settings.guildId, eventId)
|
||||
|
||||
return if (rsvp.hasRoom(userId)) {
|
||||
rsvp = rsvpService.upsertRsvp(rsvp.copyWithUserStatus(userId, goingLate = rsvp.goingLate + userId))
|
||||
|
||||
event.followupEphemeral(
|
||||
getMessage("late.success", settings),
|
||||
embedService.rsvpListEmbed(calendarEvent, rsvp, settings)
|
||||
).awaitSingle()
|
||||
event.createFollowup(getMessage("late.success", settings))
|
||||
.withEmbeds(embedService.rsvpListEmbed(calendarEvent, rsvp, settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
} else {
|
||||
rsvp = rsvpService.upsertRsvp(rsvp.copy(waitlist = rsvp.waitlist + userId))
|
||||
|
||||
event.followupEphemeral(
|
||||
getMessage("late.failure.limit", settings),
|
||||
embedService.rsvpListEmbed(calendarEvent, rsvp, settings)
|
||||
).awaitSingle()
|
||||
event.createFollowup(getMessage("late.failure.limit", settings))
|
||||
.withEmbeds(embedService.rsvpListEmbed(calendarEvent, rsvp, settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,26 +149,31 @@ class RsvpCommand(
|
||||
.get()
|
||||
|
||||
val userId = event.interaction.user.id
|
||||
val guild = event.interaction.guild.awaitSingle()
|
||||
val calendar = guild.getCalendar(calendarNumber).awaitSingleOrNull()
|
||||
val calendarEvent = calendar?.getEvent(eventId)?.awaitSingleOrNull()
|
||||
val calendar = calendarService.getCalendar(settings.guildId, calendarNumber)
|
||||
val calendarEvent = calendarService.getEvent(settings.guildId, calendarNumber, eventId)
|
||||
|
||||
// Validate required conditions
|
||||
if (calendar == null)
|
||||
return event.followupEphemeral(getCommonMsg("error.notFound.calendar", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.notFound.calendar", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
if (calendarEvent == null)
|
||||
return event.followupEphemeral(getCommonMsg("error.notFound.event", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.notFound.event", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
if (calendarEvent.isOver())
|
||||
return event.followupEphemeral(getCommonMsg("error.event.ended", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.event.ended", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
var rsvp = rsvpService.getRsvp(guild.id, eventId)
|
||||
var rsvp = rsvpService.getRsvp(settings.guildId, eventId)
|
||||
|
||||
rsvp = rsvpService.upsertRsvp(rsvp.copyWithUserStatus(userId, undecided = rsvp.undecided + userId))
|
||||
|
||||
return event.followupEphemeral(
|
||||
getMessage("unsure.success", settings),
|
||||
embedService.rsvpListEmbed(calendarEvent, rsvp, settings)
|
||||
).awaitSingle()
|
||||
return event.createFollowup(getMessage("unsure.success", settings))
|
||||
.withEmbeds(embedService.rsvpListEmbed(calendarEvent, rsvp, settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
private suspend fun notGoing(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
@@ -174,26 +188,31 @@ class RsvpCommand(
|
||||
.get()
|
||||
|
||||
val userId = event.interaction.user.id
|
||||
val guild = event.interaction.guild.awaitSingle()
|
||||
val calendar = guild.getCalendar(calendarNumber).awaitSingleOrNull()
|
||||
val calendarEvent = calendar?.getEvent(eventId)?.awaitSingleOrNull()
|
||||
val calendar = calendarService.getCalendar(settings.guildId, calendarNumber)
|
||||
val calendarEvent = calendarService.getEvent(settings.guildId, calendarNumber, eventId)
|
||||
|
||||
// Validate required conditions
|
||||
if (calendar == null)
|
||||
return event.followupEphemeral(getCommonMsg("error.notFound.calendar", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.notFound.calendar", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
if (calendarEvent == null)
|
||||
return event.followupEphemeral(getCommonMsg("error.notFound.event", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.notFound.event", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
if (calendarEvent.isOver())
|
||||
return event.followupEphemeral(getCommonMsg("error.event.ended", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.event.ended", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
var rsvp = rsvpService.getRsvp(guild.id, eventId)
|
||||
var rsvp = rsvpService.getRsvp(settings.guildId, eventId)
|
||||
|
||||
rsvp = rsvpService.upsertRsvp(rsvp.copyWithUserStatus(userId, notGoing = rsvp.notGoing + userId))
|
||||
|
||||
return event.followupEphemeral(
|
||||
getMessage("notGoing.success", settings),
|
||||
embedService.rsvpListEmbed(calendarEvent, rsvp, settings)
|
||||
).awaitSingle()
|
||||
return event.createFollowup(getMessage("notGoing.success", settings))
|
||||
.withEmbeds(embedService.rsvpListEmbed(calendarEvent, rsvp, settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
private suspend fun remove(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
@@ -208,26 +227,31 @@ class RsvpCommand(
|
||||
.get()
|
||||
|
||||
val userId = event.interaction.user.id
|
||||
val guild = event.interaction.guild.awaitSingle()
|
||||
val calendar = guild.getCalendar(calendarNumber).awaitSingleOrNull()
|
||||
val calendarEvent = calendar?.getEvent(eventId)?.awaitSingleOrNull()
|
||||
val calendar = calendarService.getCalendar(settings.guildId, calendarNumber)
|
||||
val calendarEvent = calendarService.getEvent(settings.guildId, calendarNumber, eventId)
|
||||
|
||||
// Validate required conditions
|
||||
if (calendar == null)
|
||||
return event.followupEphemeral(getCommonMsg("error.notFound.calendar", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.notFound.calendar", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
if (calendarEvent == null)
|
||||
return event.followupEphemeral(getCommonMsg("error.notFound.event", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.notFound.event", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
if (calendarEvent.isOver())
|
||||
return event.followupEphemeral(getCommonMsg("error.event.ended", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.event.ended", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
var rsvp = rsvpService.getRsvp(guild.id, eventId)
|
||||
var rsvp = rsvpService.getRsvp(settings.guildId, eventId)
|
||||
|
||||
rsvp = rsvpService.upsertRsvp(rsvp.copyWithUserStatus(userId))
|
||||
|
||||
return event.followupEphemeral(
|
||||
getMessage("remove.success", settings),
|
||||
embedService.rsvpListEmbed(calendarEvent, rsvp, settings)
|
||||
).awaitSingle()
|
||||
return event.createFollowup(getMessage("remove.success", settings))
|
||||
.withEmbeds(embedService.rsvpListEmbed(calendarEvent, rsvp, settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
private suspend fun list(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
@@ -241,19 +265,25 @@ class RsvpCommand(
|
||||
.map(ApplicationCommandInteractionOptionValue::asString)
|
||||
.get()
|
||||
|
||||
val guild = event.interaction.guild.awaitSingle()
|
||||
val calendar = guild.getCalendar(calendarNumber).awaitSingleOrNull()
|
||||
val calendarEvent = calendar?.getEvent(eventId)?.awaitSingleOrNull()
|
||||
val calendar = calendarService.getCalendar(settings.guildId, calendarNumber)
|
||||
val calendarEvent = calendarService.getEvent(settings.guildId, calendarNumber, eventId)
|
||||
|
||||
// Validate required conditions
|
||||
if (calendar == null)
|
||||
return event.followupEphemeral(getCommonMsg("error.notFound.calendar", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.notFound.calendar", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
if (calendarEvent == null)
|
||||
return event.followupEphemeral(getCommonMsg("error.notFound.event", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.notFound.event", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
val rsvp = rsvpService.getRsvp(guild.id, eventId)
|
||||
val rsvp = rsvpService.getRsvp(settings.guildId, eventId)
|
||||
|
||||
return event.followupEphemeral(embedService.rsvpListEmbed(calendarEvent, rsvp, settings)).awaitSingle()
|
||||
return event.createFollowup()
|
||||
.withEmbeds(embedService.rsvpListEmbed(calendarEvent, rsvp, settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
private suspend fun limit(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
@@ -275,36 +305,45 @@ class RsvpCommand(
|
||||
.get()
|
||||
|
||||
// Validate control role first to reduce work
|
||||
val hasControlRole = event.interaction.member.get().hasControlRole().awaitSingle()
|
||||
val hasControlRole = permissionService.hasControlRole(settings.guildId, event.interaction.user.id)
|
||||
if (!hasControlRole)
|
||||
return event.followupEphemeral(getCommonMsg("error.perms.privileged", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.perms.privileged", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
|
||||
val guild = event.interaction.guild.awaitSingle()
|
||||
val calendar = guild.getCalendar(calendarNumber).awaitSingleOrNull()
|
||||
val calendarEvent = calendar?.getEvent(eventId)?.awaitSingleOrNull()
|
||||
val calendar = calendarService.getCalendar(settings.guildId, calendarNumber)
|
||||
val calendarEvent = calendarService.getEvent(settings.guildId, calendarNumber, eventId)
|
||||
|
||||
// Validate required conditions
|
||||
if (calendar == null)
|
||||
return event.followupEphemeral(getCommonMsg("error.notFound.calendar", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.notFound.calendar", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
if (calendarEvent == null)
|
||||
return event.followupEphemeral(getCommonMsg("error.notFound.event", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.notFound.event", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
if (calendarEvent.isOver())
|
||||
return event.followupEphemeral(getCommonMsg("error.event.ended", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.event.ended", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
var rsvp = rsvpService.getRsvp(guild.id, eventId)
|
||||
var rsvp = rsvpService.getRsvp(settings.guildId, eventId)
|
||||
rsvp = rsvpService.upsertRsvp(rsvp.copy(limit = limit))
|
||||
|
||||
|
||||
return event.followupEphemeral(
|
||||
getMessage("limit.success", settings, limit.toString()),
|
||||
embedService.rsvpListEmbed(calendarEvent, rsvp, settings)
|
||||
).awaitSingle()
|
||||
return event.createFollowup(getMessage("limit.success", settings, limit.toString()))
|
||||
.withEmbeds(embedService.rsvpListEmbed(calendarEvent, rsvp, settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
private suspend fun role(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
if (!settings.patronGuild)
|
||||
return event.followupEphemeral(getCommonMsg("error.patronOnly", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.patronOnly", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
val calendarNumber = event.options[0].getOption("calendar")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
@@ -321,29 +360,39 @@ class RsvpCommand(
|
||||
|
||||
|
||||
// Validate control role first to reduce work
|
||||
val hasElevatedPerms = event.interaction.member.get().hasElevatedPermissions().awaitSingle()
|
||||
val hasElevatedPerms = permissionService.hasElevatedPermissions(settings.guildId, event.interaction.user.id)
|
||||
if (!hasElevatedPerms)
|
||||
return event.followupEphemeral(getCommonMsg("error.perms.elevated", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.perms.elevated", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
val guild = event.interaction.guild.awaitSingle()
|
||||
val calendar = guild.getCalendar(calendarNumber).awaitSingleOrNull()
|
||||
val calendarEvent = calendar?.getEvent(eventId)?.awaitSingleOrNull()
|
||||
val calendar = calendarService.getCalendar(settings.guildId, calendarNumber)
|
||||
val calendarEvent = calendarService.getEvent(settings.guildId, calendarNumber, eventId)
|
||||
|
||||
// Validate required conditions
|
||||
if (calendar == null)
|
||||
return event.followupEphemeral(getCommonMsg("error.notFound.calendar", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.notFound.calendar", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
if (calendarEvent == null)
|
||||
return event.followupEphemeral(getCommonMsg("error.notFound.event", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.notFound.event", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
if (calendarEvent.isOver())
|
||||
return event.followupEphemeral(getCommonMsg("error.event.ended", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.event.ended", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
|
||||
var rsvp = rsvpService.getRsvp(guild.id, eventId)
|
||||
var rsvp = rsvpService.getRsvp(settings.guildId, eventId)
|
||||
rsvp = rsvpService.upsertRsvp(rsvp.copy(role = if (role.isEveryone) null else role.id))
|
||||
|
||||
val embed = embedService.rsvpListEmbed(calendarEvent, rsvp, settings)
|
||||
val message = if (role.isEveryone) getMessage("role.success.remove", settings) else getMessage("role.success.set", settings, role.name)
|
||||
|
||||
return if (role.isEveryone) event.followupEphemeral(getMessage("role.success.remove", settings), embed).awaitSingle()
|
||||
else event.followupEphemeral(getMessage("role.success.set", settings, role.name), embed).awaitSingle()
|
||||
return event.createFollowup(message)
|
||||
.withEmbeds(embed)
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,91 +4,99 @@ 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 kotlinx.coroutines.reactor.awaitSingle
|
||||
import org.dreamexposure.discal.client.commands.SlashCommand
|
||||
import org.dreamexposure.discal.client.message.embed.SettingsEmbed
|
||||
import org.dreamexposure.discal.core.database.DatabaseManager
|
||||
import org.dreamexposure.discal.core.enums.announcement.AnnouncementStyle
|
||||
import org.dreamexposure.discal.core.business.EmbedService
|
||||
import org.dreamexposure.discal.core.business.GuildSettingsService
|
||||
import org.dreamexposure.discal.core.business.PermissionService
|
||||
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.`object`.new.GuildSettings
|
||||
import org.dreamexposure.discal.core.utils.getCommonMsg
|
||||
import org.springframework.stereotype.Component
|
||||
import reactor.core.publisher.Mono
|
||||
import java.util.*
|
||||
|
||||
@Component
|
||||
class SettingsCommand : SlashCommand {
|
||||
class SettingsCommand(
|
||||
private val settingsService: GuildSettingsService,
|
||||
private val embedService: EmbedService,
|
||||
private val permissionService: PermissionService,
|
||||
) : SlashCommand {
|
||||
override val name = "settings"
|
||||
override val hasSubcommands = true
|
||||
override val ephemeral = true
|
||||
|
||||
@Deprecated("Use new handleSuspend for K-coroutines")
|
||||
override fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
//Check if user has permission to use this
|
||||
return event.interaction.member.get().hasElevatedPermissions().flatMap { hasPerm ->
|
||||
if (hasPerm) {
|
||||
return@flatMap when (event.options[0].name) {
|
||||
"view" -> viewSubcommand(event, settings)
|
||||
"role" -> roleSubcommand(event, settings)
|
||||
"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.
|
||||
}
|
||||
} else {
|
||||
event.followupEphemeral(getCommonMsg("error.perms.elevated", settings))
|
||||
}
|
||||
override suspend fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
// Validate permissions
|
||||
val hasElevatedPerms = permissionService.hasElevatedPermissions(settings.guildId, event.interaction.user.id)
|
||||
if (!hasElevatedPerms) return event.createFollowup(getCommonMsg("error.perms.elevated", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
return when (event.options[0].name) {
|
||||
"view" -> view(event, settings)
|
||||
"role" -> role(event, settings)
|
||||
"announcement-style" -> announcementStyle(event, settings)
|
||||
"language" -> language(event, settings)
|
||||
"time-format" -> timeFormat(event, settings)
|
||||
"keep-event-duration" -> eventKeepDuration(event, settings)
|
||||
"branding" -> branding(event, settings)
|
||||
else -> throw IllegalStateException("Invalid subcommand specified")
|
||||
}
|
||||
}
|
||||
|
||||
private fun viewSubcommand(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
return event.interaction.guild
|
||||
.flatMap { SettingsEmbed.getView(it, settings) }
|
||||
.flatMap(event::followup)
|
||||
private suspend fun view(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
return event.createFollowup()
|
||||
.withEmbeds(embedService.settingsEmbeds(settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
private fun roleSubcommand(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
return Mono.justOrEmpty(event.options[0].getOption("role"))
|
||||
.map { it.value.get() }
|
||||
.flatMap(ApplicationCommandInteractionOptionValue::asRole)
|
||||
.doOnNext { settings.controlRole = it.id.asString() }
|
||||
.flatMap { role ->
|
||||
DatabaseManager.updateSettings(settings).then(
|
||||
event.followupEphemeral(getMessage("role.success", settings, role.name))
|
||||
)
|
||||
}
|
||||
private suspend fun role(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
val roleId = event.options[0].getOption("role")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asSnowflake)
|
||||
.orElse(settings.guildId)
|
||||
|
||||
val newSettings= settingsService.upsertSettings(settings.copy(controlRole = roleId))
|
||||
|
||||
return event.createFollowup(getMessage("role.success", settings))
|
||||
.withEmbeds(embedService.settingsEmbeds(newSettings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
private fun announcementStyleSubcommand(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
val announcementStyle = event.options[0].getOption("style")
|
||||
private suspend fun announcementStyle(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
val style = event.options[0].getOption("style")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asLong)
|
||||
.map(Long::toInt)
|
||||
.map(AnnouncementStyle::fromValue)
|
||||
.map { v -> GuildSettings.AnnouncementStyle.entries.first { it.value == v } }
|
||||
.get()
|
||||
|
||||
settings.announcementStyle = announcementStyle
|
||||
val newSettings = settingsService.upsertSettings(settings.copy(interfaceStyle = settings.interfaceStyle.copy(announcementStyle = style)))
|
||||
|
||||
return DatabaseManager.updateSettings(settings)
|
||||
.flatMap { event.followupEphemeral(getMessage("style.success", settings, announcementStyle.name)) }
|
||||
return event.createFollowup(getMessage("style.success", settings, style.name))
|
||||
.withEmbeds(embedService.settingsEmbeds(newSettings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
private fun languageSubcommand(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
val lang = event.options[0].getOption("lang")
|
||||
private suspend fun language(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
val locale = event.options[0].getOption("lang")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asString)
|
||||
.map(Locale::forLanguageTag)
|
||||
.get()
|
||||
|
||||
settings.lang = lang
|
||||
val newSettings = settingsService.upsertSettings(settings.copy(locale = locale))
|
||||
|
||||
return DatabaseManager.updateSettings(settings)
|
||||
.flatMap { event.followupEphemeral(getMessage("lang.success", settings)) }
|
||||
return event.createFollowup(getMessage("lang.success", newSettings))
|
||||
.withEmbeds(embedService.settingsEmbeds(newSettings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
private fun timeFormatSubcommand(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
private suspend fun timeFormat(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
val timeFormat = event.options[0].getOption("format")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asLong)
|
||||
@@ -96,37 +104,43 @@ class SettingsCommand : SlashCommand {
|
||||
.map(TimeFormat::fromValue)
|
||||
.get()
|
||||
|
||||
settings.timeFormat = timeFormat
|
||||
val newSettings = settingsService.upsertSettings(settings.copy(interfaceStyle = settings.interfaceStyle.copy(timeFormat = timeFormat)))
|
||||
|
||||
return DatabaseManager.updateSettings(settings)
|
||||
.flatMap { event.followupEphemeral(getMessage("format.success", settings, timeFormat.name)) }
|
||||
return event.createFollowup(getMessage("format.success", settings, timeFormat.name))
|
||||
.withEmbeds(embedService.settingsEmbeds(newSettings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
private fun eventKeepDurationSubcommand(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
private suspend fun eventKeepDuration(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
val keepDuration = event.options[0].getOption("value")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asBoolean)
|
||||
.get()
|
||||
|
||||
settings.eventKeepDuration = keepDuration
|
||||
val newSettings = settingsService.upsertSettings(settings.copy(eventKeepDuration = keepDuration))
|
||||
|
||||
return DatabaseManager.updateSettings(settings)
|
||||
.flatMap { event.followupEphemeral(getMessage("eventKeepDuration.success.$keepDuration", settings)) }
|
||||
return event.createFollowup(getMessage("eventKeepDuration.success.$keepDuration", settings))
|
||||
.withEmbeds(embedService.settingsEmbeds(newSettings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
private fun brandingSubcommand(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
return if (settings.patronGuild) {
|
||||
val useBranding = event.options[0].getOption("use")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asBoolean)
|
||||
.get()
|
||||
private suspend fun branding(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
val useBranding = event.options[0].getOption("use")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asBoolean)
|
||||
.get()
|
||||
|
||||
settings.branded = useBranding
|
||||
if (!settings.patronGuild) return event.createFollowup(getCommonMsg("error.patronOnly", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
DatabaseManager.updateSettings(settings)
|
||||
.flatMap { event.followupEphemeral(getMessage("brand.success", settings, "$useBranding")) }
|
||||
} else {
|
||||
event.followupEphemeral(getCommonMsg("error.patronOnly", settings))
|
||||
}
|
||||
val newSettings = settingsService.upsertSettings(settings.copy(interfaceStyle = settings.interfaceStyle.copy(branded = useBranding)))
|
||||
|
||||
return event.createFollowup(getMessage("brand.success", settings, "$useBranding"))
|
||||
.withEmbeds(embedService.settingsEmbeds(newSettings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,38 +5,39 @@ import discord4j.core.`object`.command.ApplicationCommandInteractionOption
|
||||
import discord4j.core.`object`.command.ApplicationCommandInteractionOptionValue
|
||||
import discord4j.core.`object`.entity.Message
|
||||
import kotlinx.coroutines.reactor.awaitSingle
|
||||
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
||||
import org.dreamexposure.discal.client.commands.SlashCommand
|
||||
import org.dreamexposure.discal.core.business.CalendarService
|
||||
import org.dreamexposure.discal.core.business.EmbedService
|
||||
import org.dreamexposure.discal.core.extensions.discord4j.followup
|
||||
import org.dreamexposure.discal.core.extensions.discord4j.getCalendar
|
||||
import org.dreamexposure.discal.core.`object`.GuildSettings
|
||||
import org.dreamexposure.discal.core.`object`.new.GuildSettings
|
||||
import org.dreamexposure.discal.core.utils.getCommonMsg
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class TimeCommand(
|
||||
private val embedService: EmbedService,
|
||||
private val calendarService: CalendarService,
|
||||
) : SlashCommand {
|
||||
override val name = "time"
|
||||
override val hasSubcommands = false
|
||||
override val ephemeral = true
|
||||
|
||||
override suspend fun suspendHandle(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
override suspend fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
val calendarNumber = event.getOption("calendar")
|
||||
.flatMap(ApplicationCommandInteractionOption::getValue)
|
||||
.map(ApplicationCommandInteractionOptionValue::asLong)
|
||||
.map(Long::toInt)
|
||||
.orElse(1)
|
||||
|
||||
|
||||
val calendar = event.interaction.guild.flatMap {
|
||||
it.getCalendar(calendarNumber)
|
||||
}.awaitSingleOrNull()
|
||||
val calendar = calendarService.getCalendar(settings.guildId, calendarNumber)
|
||||
if (calendar == null) {
|
||||
return event.followup(getCommonMsg("error.notFound.calendar", settings)).awaitSingle()
|
||||
return event.createFollowup(getCommonMsg("error.notFound.calendar", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
return event.followup(embedService.calendarTimeEmbed(calendar, settings)).awaitSingle()
|
||||
return event.createFollowup()
|
||||
.withEmbeds(embedService.calendarTimeEmbed(calendar, settings))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,49 @@
|
||||
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 kotlinx.coroutines.reactor.awaitSingle
|
||||
import org.dreamexposure.discal.client.commands.SlashCommand
|
||||
import org.dreamexposure.discal.core.business.CalendarService
|
||||
import org.dreamexposure.discal.core.business.PermissionService
|
||||
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.`object`.new.GuildSettings
|
||||
import org.dreamexposure.discal.core.utils.getCommonMsg
|
||||
import org.springframework.stereotype.Component
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
@Component
|
||||
class AddCalCommand : SlashCommand {
|
||||
class AddCalCommand(
|
||||
private val calendarService: CalendarService,
|
||||
private val permissionService: PermissionService,
|
||||
) : SlashCommand {
|
||||
override val name = "addcal"
|
||||
override val hasSubcommands = false
|
||||
override val ephemeral = true
|
||||
|
||||
@Deprecated("Use new handleSuspend for K-coroutines")
|
||||
override fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
|
||||
override suspend fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Message {
|
||||
//TODO: Remove dev-only and switch to patron-only once this is completed
|
||||
return if (settings.devGuild) {
|
||||
Mono.justOrEmpty(event.interaction.member).filterWhen(Member::hasElevatedPermissions).flatMap {
|
||||
//Check if a calendar can be added since non-premium only allows 1 calendar.
|
||||
event.interaction.guild.filterWhen(Guild::canAddCalendar).flatMap {
|
||||
event.followupEphemeral(getMessage("response.start", settings, getLink(settings)))
|
||||
}.switchIfEmpty(event.followupEphemeral(getCommonMsg("error.calendar.max", settings)))
|
||||
}.switchIfEmpty(event.followupEphemeral(getCommonMsg("error.perms.elevated", settings)))
|
||||
} else {
|
||||
event.followupEphemeral(getCommonMsg("error.disabled", settings))
|
||||
}
|
||||
if (!settings.devGuild) return event.createFollowup(getCommonMsg("error.disabled", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
// Validate permissions
|
||||
val hasElevatedPerms = permissionService.hasElevatedPermissions(settings.guildId, event.interaction.user.id)
|
||||
if (!hasElevatedPerms)
|
||||
return event.createFollowup(getCommonMsg("error.perms.elevated", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
val canAddCalendar = calendarService.canAddNewCalendar(settings.guildId)
|
||||
if (!canAddCalendar) return event.createFollowup(getCommonMsg("error.calendar.max", settings.locale))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
|
||||
return event.createFollowup(getMessage("response.start", settings, getLink(settings)))
|
||||
.withEphemeral(ephemeral)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
private fun getLink(settings: GuildSettings): String {
|
||||
return "${Config.URL_BASE.getString()}/dashboard/${settings.guildID.asString()}/calendar/new?type=1&step=0"
|
||||
return "${Config.URL_BASE.getString()}/dashboard/${settings.guildId.asLong()}/calendar/new?type=1&step=0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package org.dreamexposure.discal.client.config
|
||||
|
||||
import org.dreamexposure.discal.core.`object`.Wizard
|
||||
import org.dreamexposure.discal.core.`object`.calendar.PreCalendar
|
||||
import org.dreamexposure.discal.core.`object`.event.PreEvent
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
@Configuration
|
||||
class DisCalConfig {
|
||||
@Bean
|
||||
fun calendarWizard(): Wizard<PreCalendar> = Wizard()
|
||||
|
||||
@Bean
|
||||
fun eventWizard(): Wizard<PreEvent> = Wizard()
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.dreamexposure.discal.client.config
|
||||
|
||||
import discord4j.common.ReactorResources
|
||||
import discord4j.common.store.Store
|
||||
import discord4j.common.store.legacy.LegacyStoreLayout
|
||||
import discord4j.core.DiscordClient
|
||||
@@ -40,7 +41,10 @@ class DiscordConfig {
|
||||
stores: StoreService
|
||||
): GatewayDiscordClient {
|
||||
return DiscordClientBuilder.create(Config.SECRET_BOT_TOKEN.getString())
|
||||
.build().gateway()
|
||||
.setReactorResources(ReactorResources.builder()
|
||||
.httpClient(ReactorResources.DEFAULT_HTTP_CLIENT.get().metrics(Config.INTEGRATIONS_REACTOR_METRICS.getBoolean()) { s -> s })
|
||||
.build()
|
||||
).build().gateway()
|
||||
.setEnabledIntents(getIntents())
|
||||
.setSharding(getStrategy())
|
||||
.setStore(Store.fromLayout(LegacyStoreLayout.of(stores)))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.dreamexposure.discal.client.interaction
|
||||
|
||||
import discord4j.core.event.domain.interaction.InteractionCreateEvent
|
||||
import org.dreamexposure.discal.core.`object`.GuildSettings
|
||||
import org.dreamexposure.discal.core.`object`.new.GuildSettings
|
||||
|
||||
interface InteractionHandler<T: InteractionCreateEvent> {
|
||||
val ids: Array<String>
|
||||
|
||||
@@ -4,7 +4,7 @@ import discord4j.core.event.domain.interaction.ButtonInteractionEvent
|
||||
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
||||
import org.dreamexposure.discal.core.business.StaticMessageService
|
||||
import org.dreamexposure.discal.core.logger.LOGGER
|
||||
import org.dreamexposure.discal.core.`object`.GuildSettings
|
||||
import org.dreamexposure.discal.core.`object`.new.GuildSettings
|
||||
import org.dreamexposure.discal.core.utils.getCommonMsg
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@@ -21,11 +21,13 @@ class StaticMessageRefreshButton(
|
||||
.withEphemeral(true)
|
||||
.awaitSingleOrNull()
|
||||
|
||||
staticMessageService.updateStaticMessage(settings.guildID, event.messageId)
|
||||
staticMessageService.updateStaticMessage(settings.guildId, event.messageId)
|
||||
} catch (ex: Exception) {
|
||||
LOGGER.error("Error handling static message refresh button | guildId:${settings.guildID.asLong()} | messageId: ${event.messageId.asLong()}", ex)
|
||||
LOGGER.error("Error handling static message refresh button | guildId:${settings.guildId.asLong()} | messageId: ${event.messageId.asLong()}", ex)
|
||||
|
||||
event.createFollowup(getCommonMsg("error.unknown", settings.getLocale()))
|
||||
event.createFollowup(getCommonMsg("error.unknown", settings.locale))
|
||||
.withEphemeral(true)
|
||||
.awaitSingleOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.dreamexposure.discal.client.listeners.discord
|
||||
|
||||
import discord4j.core.event.domain.message.MessageCreateEvent
|
||||
import discord4j.core.`object`.entity.Guild
|
||||
import discord4j.core.`object`.entity.Message
|
||||
import discord4j.core.`object`.entity.User
|
||||
import kotlinx.coroutines.reactor.awaitSingle
|
||||
@@ -9,11 +8,12 @@ import kotlinx.coroutines.reactor.awaitSingleOrNull
|
||||
import org.dreamexposure.discal.core.business.AnnouncementService
|
||||
import org.dreamexposure.discal.core.business.CalendarService
|
||||
import org.dreamexposure.discal.core.business.EmbedService
|
||||
import org.dreamexposure.discal.core.extensions.discord4j.getSettings
|
||||
import org.dreamexposure.discal.core.business.GuildSettingsService
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class BotMentionListener(
|
||||
private val settingsService: GuildSettingsService,
|
||||
private val announcementService: AnnouncementService,
|
||||
private val calendarService: CalendarService,
|
||||
private val embedService: EmbedService,
|
||||
@@ -23,14 +23,17 @@ class BotMentionListener(
|
||||
&& !event.message.author.map(User::isBot).orElse(false) // Not from a bot
|
||||
&& onlyMentionsBot(event.message)
|
||||
) {
|
||||
val settings = event.guild.flatMap(Guild::getSettings).awaitSingle()
|
||||
val settings = settingsService.getSettings(event.guildId.get())
|
||||
val announcementCount = announcementService.getAnnouncementCount()
|
||||
val calendarCount = calendarService.getCalendarCount()
|
||||
val guildCount = event.client.guilds.count().awaitSingle()
|
||||
val channel = event.message.channel.awaitSingle()
|
||||
|
||||
val embed = embedService.discalInfoEmbed(settings, calendarCount, announcementCount)
|
||||
val embed = embedService.discalInfoEmbed(settings, guildCount, calendarCount, announcementCount)
|
||||
|
||||
channel.createMessage(embed).awaitSingleOrNull()
|
||||
channel.createMessage(embed)
|
||||
.withMessageReference(event.message.id)
|
||||
.awaitSingleOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package org.dreamexposure.discal.client.listeners.discord
|
||||
|
||||
import discord4j.core.event.domain.interaction.ButtonInteractionEvent
|
||||
import kotlinx.coroutines.reactor.awaitSingle
|
||||
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
||||
import org.dreamexposure.discal.client.interaction.InteractionHandler
|
||||
import org.dreamexposure.discal.core.business.GuildSettingsService
|
||||
import org.dreamexposure.discal.core.business.MetricService
|
||||
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.getCommonMsg
|
||||
@@ -16,6 +15,7 @@ import java.util.*
|
||||
@Component
|
||||
class ButtonInteractionListener(
|
||||
private val buttons: List<InteractionHandler<ButtonInteractionEvent>>,
|
||||
private val settingsService: GuildSettingsService,
|
||||
private val metricService: MetricService,
|
||||
): EventListener<ButtonInteractionEvent> {
|
||||
override suspend fun handle(event: ButtonInteractionEvent) {
|
||||
@@ -31,7 +31,7 @@ class ButtonInteractionListener(
|
||||
|
||||
if (button != null) {
|
||||
try {
|
||||
val settings = DatabaseManager.getSettings(event.interaction.guildId.get()).awaitSingle()
|
||||
val settings = settingsService.getSettings(event.interaction.guildId.get())
|
||||
|
||||
button.handle(event, settings)
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -4,16 +4,18 @@ 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.business.GuildSettingsService
|
||||
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<GuildCreateEvent> {
|
||||
class GuildCreateListener(
|
||||
private val settingsService: GuildSettingsService,
|
||||
objectMapper: ObjectMapper
|
||||
) : EventListener<GuildCreateEvent> {
|
||||
private final val premiumCommands: List<ApplicationCommandRequest>
|
||||
private final val devCommands: List<ApplicationCommandRequest>
|
||||
|
||||
@@ -39,9 +41,9 @@ class GuildCreateListener(objectMapper: ObjectMapper) : EventListener<GuildCreat
|
||||
}
|
||||
|
||||
override suspend fun handle(event: GuildCreateEvent) {
|
||||
val settings = DatabaseManager.getSettings(event.guild.id).awaitSingle()
|
||||
val guildId = event.guild.id
|
||||
val settings = settingsService.getSettings(guildId)
|
||||
val appService = event.client.restClient.applicationService
|
||||
val guildId = settings.guildID.asLong()
|
||||
val appId = event.client.selfId.asLong()
|
||||
|
||||
val commands = mutableListOf<ApplicationCommandRequest>()
|
||||
@@ -49,9 +51,9 @@ class GuildCreateListener(objectMapper: ObjectMapper) : EventListener<GuildCreat
|
||||
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) }
|
||||
appService.bulkOverwriteGuildApplicationCommand(appId, guildId.asLong(), commands)
|
||||
.doOnNext { LOGGER.debug("Bulk guild overwrite read: {} | {}", it.name(), guildId) }
|
||||
.doOnError { LOGGER.error(DEFAULT, "Bulk guild overwrite failed | ${guildId.asLong()}", it) }
|
||||
.then()
|
||||
.awaitSingleOrNull()
|
||||
}
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
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.business.GuildSettingsService
|
||||
import org.dreamexposure.discal.core.business.RsvpService
|
||||
import org.dreamexposure.discal.core.database.DatabaseManager
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class RoleDeleteListener(
|
||||
private val rsvpService: RsvpService,
|
||||
private val settingsService: GuildSettingsService,
|
||||
) : EventListener<RoleDeleteEvent> {
|
||||
|
||||
override suspend fun handle(event: RoleDeleteEvent) {
|
||||
rsvpService.removeRoleForAll(event.guildId, event.roleId)
|
||||
|
||||
DatabaseManager.getSettings(event.guildId)
|
||||
.filter { !"everyone".equals(it.controlRole, true) }
|
||||
.filter { event.roleId == Snowflake.of(it.controlRole) }
|
||||
.doOnNext { it.controlRole = "everyone" }
|
||||
.flatMap(DatabaseManager::updateSettings)
|
||||
.awaitSingleOrNull()
|
||||
val settings = settingsService.getSettings(event.guildId)
|
||||
if (settings.controlRole != null && event.roleId == settings.controlRole) {
|
||||
settingsService.upsertSettings(settings.copy(controlRole = null))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
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.business.GuildSettingsService
|
||||
import org.dreamexposure.discal.core.business.MetricService
|
||||
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.getCommonMsg
|
||||
@@ -16,6 +15,7 @@ import java.util.*
|
||||
@Component
|
||||
class SlashCommandListener(
|
||||
private val commands: List<SlashCommand>,
|
||||
private val settingsService: GuildSettingsService,
|
||||
private val metricService: MetricService,
|
||||
) : EventListener<ChatInputInteractionEvent> {
|
||||
|
||||
@@ -32,12 +32,13 @@ class SlashCommandListener(
|
||||
val subCommand = if (command?.hasSubcommands == true) event.options[0].name else null
|
||||
|
||||
if (command != null) {
|
||||
event.deferReply().withEphemeral(command.ephemeral).awaitSingleOrNull()
|
||||
if (command.shouldDefer(event)) event.deferReply().withEphemeral(command.ephemeral).awaitSingleOrNull()
|
||||
|
||||
try {
|
||||
val settings = DatabaseManager.getSettings(event.interaction.guildId.get()).awaitSingle()
|
||||
|
||||
command.suspendHandle(event, settings)
|
||||
val settings = settingsService.getSettings(event.interaction.guildId.get())
|
||||
|
||||
command.handle(event, settings)
|
||||
} catch (e: Exception) {
|
||||
LOGGER.error(DEFAULT, "Error handling slash command | $event", e)
|
||||
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package org.dreamexposure.discal.client.message.embed
|
||||
|
||||
import discord4j.core.`object`.entity.Guild
|
||||
import discord4j.core.spec.EmbedCreateSpec
|
||||
import org.dreamexposure.discal.core.entities.Calendar
|
||||
import org.dreamexposure.discal.core.extensions.embedDescriptionSafe
|
||||
import org.dreamexposure.discal.core.extensions.embedFieldSafe
|
||||
import org.dreamexposure.discal.core.extensions.embedTitleSafe
|
||||
import org.dreamexposure.discal.core.extensions.toMarkdown
|
||||
import org.dreamexposure.discal.core.`object`.GuildSettings
|
||||
import org.dreamexposure.discal.core.`object`.calendar.PreCalendar
|
||||
import org.dreamexposure.discal.core.utils.GlobalVal.discalColor
|
||||
import org.dreamexposure.discal.core.utils.getCommonMsg
|
||||
|
||||
object CalendarEmbed : EmbedMaker {
|
||||
|
||||
@Deprecated("Use replacement in EmbedService")
|
||||
fun link(guild: Guild, settings: GuildSettings, calendar: Calendar): EmbedCreateSpec {
|
||||
val builder = defaultBuilder(guild, settings)
|
||||
//Handle optional fields
|
||||
if (calendar.name.isNotBlank())
|
||||
builder.title(calendar.name.toMarkdown().embedTitleSafe())
|
||||
if (calendar.description.isNotBlank())
|
||||
builder.description(calendar.description.toMarkdown().embedDescriptionSafe())
|
||||
|
||||
return builder.addField(getMessage("calendar", "link.field.timezone", settings), calendar.zoneName, false)
|
||||
.addField(getMessage("calendar", "link.field.host", settings), calendar.calendarData.host.name, true)
|
||||
.addField(getMessage("calendar", "link.field.number", settings), "${calendar.calendarNumber}", true)
|
||||
.addField(getMessage("calendar", "link.field.id", settings), calendar.calendarId, false)
|
||||
.url(calendar.link)
|
||||
.footer(getMessage("calendar", "link.footer.default", settings), null)
|
||||
.color(discalColor)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun pre(guild: Guild, settings: GuildSettings, preCal: PreCalendar): EmbedCreateSpec {
|
||||
val builder = defaultBuilder(guild, settings)
|
||||
.title(getMessage("calendar", "wizard.title", settings))
|
||||
.addField(getMessage(
|
||||
"calendar", "wizard.field.name", settings),
|
||||
preCal.name.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
).addField(
|
||||
getMessage("calendar", "wizard.field.description", settings),
|
||||
preCal.description?.ifEmpty { getCommonMsg("embed.unset", settings) }?.toMarkdown()?.embedFieldSafe()
|
||||
?: getCommonMsg("embed.unset", settings),
|
||||
false
|
||||
).addField(getMessage("calendar", "wizard.field.timezone", settings),
|
||||
preCal.timezone?.id ?: getCommonMsg("embed.unset", settings),
|
||||
true
|
||||
).addField(getMessage("calendar", "wizard.field.host", settings), preCal.host.name, true)
|
||||
.footer(getMessage("calendar", "wizard.footer", settings), null)
|
||||
|
||||
if (preCal.editing)
|
||||
builder.addField(getMessage("calendar", "wizard.field.id", settings), preCal.calendar!!.calendarId, false)
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
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.config.Config
|
||||
import org.dreamexposure.discal.core.`object`.GuildSettings
|
||||
import org.dreamexposure.discal.core.utils.GlobalVal
|
||||
import org.dreamexposure.discal.core.utils.getCommonMsg
|
||||
import org.dreamexposure.discal.core.utils.getEmbedMessage
|
||||
|
||||
interface EmbedMaker {
|
||||
fun defaultBuilder(guild: Guild, settings: GuildSettings): EmbedCreateSpec.Builder {
|
||||
val builder = EmbedCreateSpec.builder()
|
||||
|
||||
if (settings.branded)
|
||||
builder.author(guild.name, Config.URL_BASE.getString(), guild.getIconUrl(Image.Format.PNG).orElse(GlobalVal.iconUrl))
|
||||
else
|
||||
builder.author(getCommonMsg("bot.name", settings), Config.URL_BASE.getString(), GlobalVal.iconUrl)
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
fun getMessage(embed: String, key: String, settings: GuildSettings, vararg args: String) =
|
||||
getEmbedMessage(embed, key, settings, *args)
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
package org.dreamexposure.discal.client.message.embed
|
||||
|
||||
import discord4j.core.`object`.entity.Guild
|
||||
import discord4j.core.spec.EmbedCreateSpec
|
||||
import org.dreamexposure.discal.core.`object`.GuildSettings
|
||||
import org.dreamexposure.discal.core.`object`.event.PreEvent
|
||||
import org.dreamexposure.discal.core.entities.Event
|
||||
import org.dreamexposure.discal.core.enums.time.DiscordTimestampFormat.LONG_DATETIME
|
||||
import org.dreamexposure.discal.core.extensions.*
|
||||
import org.dreamexposure.discal.core.utils.getCommonMsg
|
||||
|
||||
object EventEmbed : EmbedMaker {
|
||||
fun getFull(guild: Guild, settings: GuildSettings, event: Event): EmbedCreateSpec {
|
||||
val builder = defaultBuilder(guild, settings)
|
||||
.footer(getMessage("event", "full.footer", settings, event.eventId), null)
|
||||
.color(event.color.asColor())
|
||||
|
||||
if (event.name.isNotBlank())
|
||||
builder.title(event.name.toMarkdown().embedTitleSafe())
|
||||
if (event.description.isNotBlank())
|
||||
builder.description(event.description.toMarkdown().embedDescriptionSafe())
|
||||
|
||||
builder.addField(
|
||||
getMessage("event", "full.field.start", settings),
|
||||
event.start.asDiscordTimestamp(LONG_DATETIME),
|
||||
true)
|
||||
builder.addField(
|
||||
getMessage("event", "full.field.end", settings),
|
||||
event.end.asDiscordTimestamp(LONG_DATETIME),
|
||||
true
|
||||
)
|
||||
|
||||
if (event.location.isNotBlank()) builder.addField(
|
||||
getMessage("event", "full.field.location", settings),
|
||||
event.location.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
)
|
||||
|
||||
builder.addField(getMessage("event", "full.field.cal", settings), "${event.calendar.calendarNumber}", false)
|
||||
|
||||
if (event.image.isNotEmpty())
|
||||
builder.image(event.image)
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun getCondensed(guild: Guild, settings: GuildSettings, event: Event): EmbedCreateSpec {
|
||||
val builder = defaultBuilder(guild, settings)
|
||||
.footer(getMessage("event", "con.footer", settings, event.eventId), null)
|
||||
.color(event.color.asColor())
|
||||
|
||||
if (event.name.isNotBlank())
|
||||
builder.title(event.name.toMarkdown().embedTitleSafe())
|
||||
|
||||
builder.addField(
|
||||
getMessage("event", "con.field.start", settings),
|
||||
event.start.asDiscordTimestamp(LONG_DATETIME),
|
||||
true
|
||||
)
|
||||
|
||||
if (event.location.isNotBlank()) builder.addField(
|
||||
getMessage("event", "con.field.location", settings),
|
||||
event.location.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
)
|
||||
|
||||
if (event.image.isNotBlank())
|
||||
builder.thumbnail(event.image)
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun pre(guild: Guild, settings: GuildSettings, event: PreEvent): EmbedCreateSpec {
|
||||
val builder = defaultBuilder(guild, settings)
|
||||
.title(getMessage("event", "wizard.title", settings))
|
||||
.footer(getMessage("event", "wizard.footer", settings), null)
|
||||
.color(event.color.asColor())
|
||||
|
||||
if (!event.name.isNullOrBlank()) builder.addField(
|
||||
getMessage("event", "wizard.field.name", settings),
|
||||
event.name!!.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
) else builder.addField(
|
||||
getMessage("event", "wizard.field.name", settings),
|
||||
getCommonMsg("embed.unset", settings),
|
||||
false
|
||||
)
|
||||
|
||||
if (!event.description.isNullOrBlank()) builder.addField(
|
||||
getMessage("event", "wizard.field.desc", settings),
|
||||
event.description!!.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
) else builder.addField(
|
||||
getMessage("event", "wizard.field.desc", settings),
|
||||
getCommonMsg("embed.unset", settings),
|
||||
false
|
||||
)
|
||||
|
||||
if (!event.location.isNullOrBlank()) builder.addField(
|
||||
getMessage("event", "wizard.field.location", settings),
|
||||
event.location!!.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
) else builder.addField(
|
||||
getMessage("event", "wizard.field.location", settings),
|
||||
getCommonMsg("embed.unset", settings),
|
||||
false
|
||||
)
|
||||
|
||||
if (event.start != null) builder.addField(
|
||||
getMessage("event", "wizard.field.start", settings),
|
||||
event.start!!.humanReadableFull(event.timezone, settings.timeFormat),
|
||||
true
|
||||
) else builder.addField(
|
||||
getMessage("event", "wizard.field.start", settings),
|
||||
getCommonMsg("embed.unset", settings),
|
||||
true
|
||||
)
|
||||
|
||||
if (event.end != null) builder.addField(
|
||||
getMessage("event", "wizard.field.end", settings),
|
||||
event.end!!.humanReadableFull(event.timezone, settings.timeFormat),
|
||||
true
|
||||
) else builder.addField(
|
||||
getMessage("event", "wizard.field.end", settings),
|
||||
getCommonMsg("embed.unset", settings),
|
||||
true
|
||||
)
|
||||
|
||||
if (event.recurrence != null) builder.addField(
|
||||
getMessage("event", "wizard.field.recurrence", settings),
|
||||
event.recurrence!!.toHumanReadable(),
|
||||
true
|
||||
) else if (event.editing && event.eventId != null && event.eventId!!.contains("_")) builder.addField(
|
||||
getMessage("event", "wizard.field.recurrence", settings),
|
||||
getMessage("event", "wizard.field.recurrence.child", settings, event.eventId!!.split("_")[0]),
|
||||
false,
|
||||
) else builder.addField(
|
||||
getMessage("event", "wizard.field.recurrence", settings),
|
||||
getCommonMsg("embed.unset", settings),
|
||||
true
|
||||
)
|
||||
|
||||
builder.addField(getMessage("event", "wizard.field.timezone", settings), event.timezone.id, false)
|
||||
|
||||
if (event.editing)
|
||||
builder.addField(getMessage("event", "wizard.field.id", settings), event.eventId!!, true)
|
||||
builder.addField(getMessage("event", "wizard.field.calendar", settings), event.calNumber.toString(), true)
|
||||
|
||||
if (event.image != null)
|
||||
builder.image(event.image!!)
|
||||
|
||||
val warnings = event.generateWarnings(settings)
|
||||
if (warnings.isNotEmpty()) {
|
||||
val warnText = "```fix\n${warnings.joinToString("\n")}\n```"
|
||||
builder.addField(getMessage("event", "wizard.field.warnings", settings), warnText, false)
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
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.extensions.discord4j.getControlRole
|
||||
import org.dreamexposure.discal.core.`object`.GuildSettings
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
object SettingsEmbed : EmbedMaker {
|
||||
|
||||
fun getView(guild: Guild, settings: GuildSettings): Mono<EmbedCreateSpec> {
|
||||
return guild.getControlRole().map(Role::getName).map { roleName ->
|
||||
defaultBuilder(guild, settings)
|
||||
.title(getMessage("settings", "view.title", settings))
|
||||
.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)
|
||||
.addField(getMessage("settings", "view.field.cal", settings), "${settings.maxCalendars}", true)
|
||||
.addField(getMessage("settings", "view.field.brand", settings), "${settings.branded}", false)
|
||||
.footer(getMessage("settings", "view.footer", settings), null)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,27 +14,9 @@ class Application {
|
||||
val instanceId: UUID = UUID.randomUUID()
|
||||
|
||||
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
|
||||
This has been an open issue and PR for over 3 years now, and has gone stale as of March 3rd 2021.
|
||||
So, in order to get the pod ordinal since its not directly exposed, we have to get the hostname, and parse
|
||||
the ordinal out of that.
|
||||
To make sure we don't use this when running anywhere but inside of k8s, we are mapping the hostname to an env
|
||||
variable SHARD_POD_NAME and if that is present, parsing it for the pod ordinal to tell the bot its shard index.
|
||||
This will be removed when/if they add this feature directly and SHARD_INDEX will be an env variable...
|
||||
*/
|
||||
|
||||
//Check if we are running in k8s or not...
|
||||
val shardPodName = System.getenv("SHARD_POD_NAME")
|
||||
return if (shardPodName != null) {
|
||||
//In k8s, parse this shit
|
||||
val parts = shardPodName.split("-")
|
||||
parts[parts.size -1].toInt()
|
||||
} else {
|
||||
//Fall back to config value
|
||||
Config.SHARD_INDEX.getInt()
|
||||
}
|
||||
val k8sPodIndex = System.getenv("KUBERNETES_POD_INDEX")
|
||||
return k8sPodIndex?.toInt() ?: // Fall back to config
|
||||
Config.SHARD_INDEX.getInt()
|
||||
}
|
||||
|
||||
fun getShardCount(): Int {
|
||||
|
||||
@@ -8,15 +8,14 @@ import kotlinx.coroutines.reactor.awaitSingle
|
||||
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
||||
import org.dreamexposure.discal.AnnouncementCache
|
||||
import org.dreamexposure.discal.AnnouncementWizardStateCache
|
||||
import org.dreamexposure.discal.core.config.Config
|
||||
import org.dreamexposure.discal.core.database.AnnouncementData
|
||||
import org.dreamexposure.discal.core.database.AnnouncementRepository
|
||||
import org.dreamexposure.discal.core.database.DatabaseManager
|
||||
import org.dreamexposure.discal.core.entities.Calendar
|
||||
import org.dreamexposure.discal.core.entities.Event
|
||||
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`.new.Announcement
|
||||
import org.dreamexposure.discal.core.`object`.new.AnnouncementWizardState
|
||||
import org.dreamexposure.discal.core.`object`.new.Event
|
||||
import org.springframework.beans.factory.BeanFactory
|
||||
import org.springframework.beans.factory.getBean
|
||||
import org.springframework.stereotype.Component
|
||||
@@ -31,12 +30,15 @@ class AnnouncementService(
|
||||
private val announcementCache: AnnouncementCache,
|
||||
private val announcementWizardStateCache: AnnouncementWizardStateCache,
|
||||
private val embedService: EmbedService,
|
||||
private val calendarService: CalendarService,
|
||||
private val metricService: MetricService,
|
||||
private val beanFactory: BeanFactory,
|
||||
) {
|
||||
private val discordClient: DiscordClient
|
||||
get() = beanFactory.getBean()
|
||||
|
||||
private val PROCESS_GUILD_DEFAULT_UPCOMING_EVENTS_COUNT = Config.ANNOUNCEMENT_PROCESS_GUILD_DEFAULT_UPCOMING_EVENTS_COUNT.getInt()
|
||||
|
||||
suspend fun createAnnouncement(announcement: Announcement): Announcement {
|
||||
val saved = announcementRepository.save(AnnouncementData(
|
||||
announcementId = announcement.id,
|
||||
@@ -62,7 +64,7 @@ class AnnouncementService(
|
||||
return saved
|
||||
}
|
||||
|
||||
suspend fun getAnnouncementCount(): Long = announcementRepository.count().awaitSingle()
|
||||
suspend fun getAnnouncementCount(): Long = announcementRepository.countAll().awaitSingle()
|
||||
|
||||
suspend fun getAllAnnouncements(shardIndex: Int, shardCount: Int): List<Announcement> {
|
||||
return announcementRepository.findAllByShardIndexAndEnabledIsTrue(shardCount, shardIndex)
|
||||
@@ -121,9 +123,13 @@ class AnnouncementService(
|
||||
.toTypedArray()
|
||||
announcementCache.put(key = announcement.guildId, value = new)
|
||||
}
|
||||
|
||||
// Cancel any existing wizards
|
||||
cancelWizard(announcement.guildId, announcement.id)
|
||||
}
|
||||
|
||||
suspend fun deleteAnnouncement(guildId: Snowflake, id: String) {
|
||||
cancelWizard(guildId, id)
|
||||
announcementRepository.deleteByAnnouncementId(id).awaitSingleOrNull()
|
||||
|
||||
val cached = announcementCache.get(key = guildId)
|
||||
@@ -133,6 +139,7 @@ class AnnouncementService(
|
||||
}
|
||||
|
||||
suspend fun deleteAnnouncements(guildId: Snowflake, eventId: String) {
|
||||
cancelWizardByEvent(guildId, eventId)
|
||||
announcementRepository.deleteAllByGuildIdAndEventId(guildId.asLong(), eventId).awaitSingleOrNull()
|
||||
|
||||
val cached = announcementCache.get(key = guildId)
|
||||
@@ -141,6 +148,13 @@ class AnnouncementService(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteAnnouncementsForCalendarDeletion(guildId: Snowflake, calendarNumber: Int) {
|
||||
cancelWizard(guildId, calendarNumber)
|
||||
announcementRepository.deleteAllByGuildIdAndCalendarNumber(guildId.asLong(), calendarNumber).awaitSingleOrNull()
|
||||
announcementRepository.decrementCalendarsByGuildIdAndCalendarNumber(guildId.asLong(), calendarNumber).awaitSingleOrNull()
|
||||
announcementCache.evict(key = guildId)
|
||||
}
|
||||
|
||||
suspend fun sendAnnouncement(announcement: Announcement, event: Event) {
|
||||
try {
|
||||
val channel = discordClient.getChannelById(announcement.channelId)
|
||||
@@ -154,11 +168,11 @@ class AnnouncementService(
|
||||
deleteAnnouncement(announcement.guildId, announcement.id)
|
||||
return
|
||||
}
|
||||
val settings = DatabaseManager.getSettings(announcement.guildId).awaitSingle()
|
||||
|
||||
val embed = embedService.determineAnnouncementEmbed(announcement, event, settings)
|
||||
val embed = embedService.determineAnnouncementEmbed(announcement, event)
|
||||
|
||||
val message = channel.createMessage(MessageCreateRequest.builder()
|
||||
.content(announcement.subscribers.buildMentions().messageContentSafe())
|
||||
.addEmbed(embed.asRequest())
|
||||
.build()
|
||||
).awaitSingle()
|
||||
@@ -197,49 +211,39 @@ class AnnouncementService(
|
||||
val taskTimer = StopWatch()
|
||||
taskTimer.start()
|
||||
|
||||
val guild = discordClient.getGuildById(guildId)
|
||||
val calendars: MutableSet<Calendar> = mutableSetOf()
|
||||
// Since we currently can't look up upcoming events from cache cuz I dunno how, we just hold in very temporary and scoped memory at least
|
||||
val events: MutableMap<Int, List<Event>> = mutableMapOf()
|
||||
|
||||
// TODO: Need to break this out to add handling for modifiers
|
||||
getAllAnnouncements(guildId, returnDisabled = false).forEach { announcement ->
|
||||
// Get the calendar
|
||||
var calendar = calendars.firstOrNull { it.calendarNumber == announcement.calendarNumber }
|
||||
if (calendar == null) {
|
||||
calendar = guild.getCalendar(announcement.calendarNumber).awaitSingleOrNull() ?: return@forEach
|
||||
calendars.add(calendar)
|
||||
}
|
||||
|
||||
// Handle specific type first, since we don't need to fetch all events for this
|
||||
if (announcement.type == Announcement.Type.SPECIFIC) {
|
||||
val event = calendar.getEvent(announcement.eventId!!).awaitSingleOrNull() ?: return@forEach
|
||||
val event = calendarService.getEvent(guildId, announcement.calendarNumber, announcement.eventId!!) ?: return@forEach
|
||||
if (isInRange(announcement, event, maxDifference)) {
|
||||
sendAnnouncement(announcement, event)
|
||||
}
|
||||
}
|
||||
|
||||
// Get the events to filter through
|
||||
var filteredEvents = events[calendar.calendarNumber]
|
||||
var filteredEvents = events[announcement.calendarNumber]
|
||||
if (filteredEvents == null) {
|
||||
filteredEvents = calendar.getUpcomingEvents(20)
|
||||
.collectList()
|
||||
.awaitSingle()
|
||||
events[calendar.calendarNumber] = filteredEvents
|
||||
filteredEvents = calendarService.getUpcomingEvents(guildId, announcement.calendarNumber, PROCESS_GUILD_DEFAULT_UPCOMING_EVENTS_COUNT)
|
||||
events[announcement.calendarNumber] = filteredEvents
|
||||
}
|
||||
|
||||
// Handle filtering out events based on this announcement's types
|
||||
if (announcement.type == Announcement.Type.COLOR) {
|
||||
filteredEvents = filteredEvents?.filter { it.color == announcement.eventColor }
|
||||
filteredEvents = filteredEvents.filter { it.color == announcement.eventColor }
|
||||
} else if (announcement.type == Announcement.Type.RECUR) {
|
||||
filteredEvents = filteredEvents
|
||||
?.filter { it.eventId.contains("_") }
|
||||
?.filter { it.eventId.split("_")[0] == announcement.eventId }
|
||||
.filter { it.id.contains("_") }
|
||||
.filter { it.id.split("_")[0] == announcement.eventId }
|
||||
}
|
||||
|
||||
// Loop through filtered events and post any announcements in range
|
||||
filteredEvents
|
||||
?.filter { isInRange(announcement, it, maxDifference) }
|
||||
?.forEach { sendAnnouncement(announcement, it) }
|
||||
.filter { isInRange(announcement, it, maxDifference) }
|
||||
.forEach { sendAnnouncement(announcement, it) }
|
||||
|
||||
}
|
||||
|
||||
@@ -264,4 +268,16 @@ class AnnouncementService(
|
||||
.filter { it.entity.id == announcementId }
|
||||
.forEach { announcementWizardStateCache.evict(guildId, it.userId) }
|
||||
}
|
||||
|
||||
suspend fun cancelWizard(guildId: Snowflake, calendarNumber: Int) {
|
||||
announcementWizardStateCache.getAll(guildId)
|
||||
.filter { it.entity.calendarNumber == calendarNumber }
|
||||
.forEach { announcementWizardStateCache.evict(guildId, it.userId) }
|
||||
}
|
||||
|
||||
suspend fun cancelWizardByEvent(guildId: Snowflake, eventId: String) {
|
||||
announcementWizardStateCache.getAll(guildId)
|
||||
.filter { it.entity.eventId == eventId }
|
||||
.forEach { announcementWizardStateCache.evict(guildId, it.userId) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.dreamexposure.discal.core.business
|
||||
|
||||
import discord4j.common.util.Snowflake
|
||||
import org.dreamexposure.discal.core.`object`.new.Calendar
|
||||
import org.dreamexposure.discal.core.`object`.new.CalendarMetadata
|
||||
import org.dreamexposure.discal.core.`object`.new.CalendarMetadata.Host
|
||||
import org.dreamexposure.discal.core.`object`.new.Event
|
||||
import java.time.Instant
|
||||
|
||||
interface CalendarProvider {
|
||||
val host: Host
|
||||
|
||||
/////////
|
||||
/// Calendar functions
|
||||
/////////
|
||||
suspend fun getCalendar(metadata: CalendarMetadata): Calendar?
|
||||
|
||||
suspend fun createCalendar(guildId: Snowflake, spec: Calendar.CreateSpec): Calendar
|
||||
|
||||
suspend fun updateCalendar(guildId: Snowflake, metadata: CalendarMetadata, spec: Calendar.UpdateSpec): Calendar
|
||||
|
||||
suspend fun deleteCalendar(guildId: Snowflake, metadata: CalendarMetadata)
|
||||
|
||||
|
||||
/////////
|
||||
/// Event functions
|
||||
/////////
|
||||
suspend fun getEvent(calendar: Calendar, id: String): Event?
|
||||
|
||||
suspend fun getUpcomingEvents(calendar: Calendar, amount: Int): List<Event>
|
||||
|
||||
suspend fun getOngoingEvents(calendar: Calendar): List<Event>
|
||||
|
||||
suspend fun getEventsInTimeRange(calendar: Calendar, start: Instant, end: Instant): List<Event>
|
||||
|
||||
suspend fun createEvent(calendar: Calendar, spec: Event.CreateSpec): Event
|
||||
|
||||
suspend fun updateEvent(calendar: Calendar, spec: Event.UpdateSpec): Event
|
||||
|
||||
suspend fun deleteEvent(calendar: Calendar, id: String)
|
||||
}
|
||||
@@ -4,42 +4,98 @@ import discord4j.common.util.Snowflake
|
||||
import kotlinx.coroutines.reactor.awaitSingle
|
||||
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
||||
import kotlinx.coroutines.reactor.mono
|
||||
import org.dreamexposure.discal.CalendarCache
|
||||
import org.dreamexposure.discal.*
|
||||
import org.dreamexposure.discal.core.crypto.AESEncryption
|
||||
import org.dreamexposure.discal.core.database.CalendarRepository
|
||||
import org.dreamexposure.discal.core.`object`.new.Calendar
|
||||
import org.dreamexposure.discal.core.database.CalendarMetadataData
|
||||
import org.dreamexposure.discal.core.database.CalendarMetadataRepository
|
||||
import org.dreamexposure.discal.core.exceptions.NotFoundException
|
||||
import org.dreamexposure.discal.core.`object`.new.*
|
||||
import org.springframework.beans.factory.BeanFactory
|
||||
import org.springframework.beans.factory.getBean
|
||||
import org.springframework.stereotype.Component
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
@Component
|
||||
class CalendarService(
|
||||
private val calendarRepository: CalendarRepository,
|
||||
class CalendarService(
|
||||
private val calendarMetadataRepository: CalendarMetadataRepository,
|
||||
private val calendarMetadataCache: CalendarMetadataCache,
|
||||
private val eventCache: EventCache,
|
||||
private val calendarProviders: List<CalendarProvider>,
|
||||
private val calendarCache: CalendarCache,
|
||||
private val calendarWizardStateCache: CalendarWizardStateCache,
|
||||
private val eventWizardStateCache: EventWizardStateCache,
|
||||
private val settingsService: GuildSettingsService,
|
||||
private val eventMetadataService: EventMetadataService,
|
||||
private val beanFactory: BeanFactory,
|
||||
) {
|
||||
suspend fun getCalendarCount(): Long = calendarRepository.count().awaitSingle()
|
||||
// We're going to fetch beans here for a lot of services that depend on CalendarService due to architecture (plus these are used in only one place so...)
|
||||
private val staticMessageService
|
||||
get() = beanFactory.getBean<StaticMessageService>()
|
||||
private val rsvpService
|
||||
get() = beanFactory.getBean<RsvpService>()
|
||||
private val announcementService
|
||||
get() = beanFactory.getBean<AnnouncementService>()
|
||||
|
||||
suspend fun getAllCalendars(guildId: Snowflake): List<Calendar> {
|
||||
var calendars = calendarCache.get(key = guildId)?.toList()
|
||||
|
||||
/////////
|
||||
/// Calendar count
|
||||
/////////
|
||||
suspend fun getCalendarCount(): Long = calendarMetadataRepository.countAll().awaitSingle()
|
||||
|
||||
suspend fun getCalendarCount(guildId: Snowflake) = calendarMetadataRepository.countAllByGuildId(guildId.asLong()).awaitSingle()
|
||||
|
||||
/////////
|
||||
/// Calendar metadata - Prefer using full Calendar implementation
|
||||
/////////
|
||||
suspend fun getAllCalendarMetadata(guildId: Snowflake): List<CalendarMetadata> {
|
||||
var calendars = calendarMetadataCache.get(key = guildId)?.toList()
|
||||
if (calendars != null) return calendars
|
||||
|
||||
calendars = calendarRepository.findAllByGuildId(guildId.asLong())
|
||||
.flatMap { mono { Calendar(it) } }
|
||||
calendars = calendarMetadataRepository.findAllByGuildId(guildId.asLong())
|
||||
.flatMap { mono { CalendarMetadata(it) } }
|
||||
.collectList()
|
||||
.awaitSingle()
|
||||
|
||||
calendarCache.put(key = guildId, value = calendars.toTypedArray())
|
||||
calendarMetadataCache.put(key = guildId, value = calendars.toTypedArray())
|
||||
return calendars
|
||||
}
|
||||
|
||||
suspend fun getCalendar(guildId: Snowflake, number: Int): Calendar? {
|
||||
return getAllCalendars(guildId).firstOrNull { it.number == number }
|
||||
suspend fun getCalendarMetadata(guildId: Snowflake, number: Int): CalendarMetadata? {
|
||||
return getAllCalendarMetadata(guildId).firstOrNull { it.number == number }
|
||||
}
|
||||
|
||||
suspend fun updateCalendar(calendar: Calendar) {
|
||||
suspend fun createCalendarMetadata(calendar: CalendarMetadata): CalendarMetadata {
|
||||
val aes = AESEncryption(calendar.secrets.privateKey)
|
||||
val encryptedRefreshToken = aes.encrypt(calendar.secrets.refreshToken).awaitSingle()
|
||||
val encryptedAccessToken = aes.encrypt(calendar.secrets.accessToken).awaitSingle()
|
||||
|
||||
calendarRepository.updateCalendarByGuildIdAndCalendarNumber(
|
||||
calendarMetadataRepository.save(CalendarMetadataData(
|
||||
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 = encryptedAccessToken,
|
||||
refreshToken = encryptedRefreshToken,
|
||||
expiresAt = calendar.secrets.expiresAt.toEpochMilli(),
|
||||
)).flatMap { mono { CalendarMetadata(it) } }.awaitSingle()
|
||||
|
||||
val cached = calendarMetadataCache.get(key = calendar.guildId)
|
||||
if (cached != null) calendarMetadataCache.put(key = calendar.guildId, value = cached + calendar)
|
||||
|
||||
return calendar
|
||||
}
|
||||
|
||||
suspend fun updateCalendarMetadata(calendar: CalendarMetadata) {
|
||||
val aes = AESEncryption(calendar.secrets.privateKey)
|
||||
val encryptedRefreshToken = aes.encrypt(calendar.secrets.refreshToken).awaitSingle()
|
||||
val encryptedAccessToken = aes.encrypt(calendar.secrets.accessToken).awaitSingle()
|
||||
|
||||
calendarMetadataRepository.updateCalendarByGuildIdAndCalendarNumber(
|
||||
guildId = calendar.guildId.asLong(),
|
||||
calendarNumber = calendar.number,
|
||||
host = calendar.host.name,
|
||||
@@ -53,11 +109,282 @@ class CalendarService(
|
||||
expiresAt = calendar.secrets.expiresAt.toEpochMilli(),
|
||||
).awaitSingleOrNull()
|
||||
|
||||
val cached = calendarCache.get(key = calendar.guildId)
|
||||
val cached = calendarMetadataCache.get(key = calendar.guildId)
|
||||
if (cached != null) {
|
||||
val newList = cached.toMutableList()
|
||||
newList.removeIf { it.number == calendar.number }
|
||||
calendarCache.put(key = calendar.guildId,value = (newList + calendar).toTypedArray())
|
||||
calendarMetadataCache.put(key = calendar.guildId,value = (newList + calendar).toTypedArray())
|
||||
}
|
||||
|
||||
val cachedFullCalendar = calendarCache.get(calendar.guildId, calendar.number)
|
||||
if (cachedFullCalendar != null) calendarCache.put(calendar.guildId, calendar.number, cachedFullCalendar.copy(metadata = calendar))
|
||||
}
|
||||
|
||||
suspend fun getNextCalendarNumber(guildId: Snowflake): Int = getAllCalendarMetadata(guildId).size + 1
|
||||
|
||||
/////////
|
||||
/// Calendar
|
||||
/////////
|
||||
suspend fun getCalendar(guildId: Snowflake, number: Int): Calendar? {
|
||||
var calendar = calendarCache.get(guildId, number)
|
||||
if (calendar != null) return calendar
|
||||
|
||||
val metadata = getCalendarMetadata(guildId, number) ?: return null
|
||||
|
||||
calendar = calendarProviders
|
||||
.first { it.host == metadata.host }
|
||||
.getCalendar(metadata)
|
||||
if (calendar != null) calendarCache.put(guildId, number, calendar)
|
||||
|
||||
return calendar
|
||||
}
|
||||
|
||||
suspend fun getAllCalendars(guildId: Snowflake): List<Calendar> {
|
||||
return getAllCalendarMetadata(guildId).map { getCalendar(guildId, it.number)!! }
|
||||
|
||||
}
|
||||
|
||||
suspend fun createCalendar(guildId: Snowflake, spec: Calendar.CreateSpec): Calendar {
|
||||
val calendar = calendarProviders
|
||||
.first { it.host == spec.host }
|
||||
.createCalendar(guildId, spec)
|
||||
|
||||
createCalendarMetadata(calendar.metadata)
|
||||
|
||||
calendarCache.put(guildId, calendar.metadata.number, calendar)
|
||||
return calendar
|
||||
}
|
||||
|
||||
suspend fun updateCalendar(guildId: Snowflake, number: Int, spec: Calendar.UpdateSpec): Calendar {
|
||||
val metadata = getCalendarMetadata(guildId, number) ?: throw NotFoundException("Cannot update a calendar that does not exist")
|
||||
|
||||
val calendar = calendarProviders
|
||||
.first { it.host == metadata.host }
|
||||
.updateCalendar(guildId, metadata, spec)
|
||||
|
||||
calendarCache.put(guildId, calendar.metadata.number, calendar)
|
||||
|
||||
// Cancel wizards
|
||||
cancelCalendarWizard(guildId, number)
|
||||
|
||||
// Make sure static messages get updated
|
||||
staticMessageService.updateStaticMessages(guildId, number)
|
||||
|
||||
return calendar
|
||||
}
|
||||
|
||||
suspend fun deleteCalendar(guildId: Snowflake, number: Int) {
|
||||
val metadata = getCalendarMetadata(guildId, number) ?: return
|
||||
|
||||
// Delete from 3rd party locations
|
||||
calendarProviders.first { it.host == metadata.host }.deleteCalendar(guildId, metadata)
|
||||
|
||||
// Cancel any wizards
|
||||
cancelCalendarWizard(guildId, number)
|
||||
cancelEventWizard(guildId, number)
|
||||
|
||||
// Delete from db and handle "re-indexing" all calendar resources (because calendars are sequential for user-convenience)
|
||||
calendarMetadataRepository.deleteAllByGuildIdAndCalendarNumber(guildId.asLong(), number).awaitSingleOrNull()
|
||||
calendarMetadataRepository.decrementCalendarsByGuildIdAndCalendarNumber(guildId.asLong(), number).awaitSingleOrNull()
|
||||
|
||||
// Remove from caches
|
||||
calendarCache.evict(guildId, metadata.number)
|
||||
calendarMetadataCache.evict(key = guildId)
|
||||
|
||||
/*
|
||||
This is a set of calls to replicate the behavior of the old monolith db call
|
||||
that would go through all tables to handle deleting (as cascade delete constraints have not yet been added),
|
||||
and to update the calendar number references down-stream. This is again for user-convenience, or so I tell myself
|
||||
as a cope for how badly designed this project originally was and I just, can't let go of it,
|
||||
so I keep trying to fix it bit by bit <3
|
||||
*/
|
||||
eventMetadataService.deleteEventMetadataForCalendarDeletion(guildId, number)
|
||||
rsvpService.deleteRsvpForCalendarDeletion(guildId, number)
|
||||
announcementService.deleteAnnouncementsForCalendarDeletion(guildId, number)
|
||||
staticMessageService.deleteStaticMessagesForCalendarDeletion(guildId, number)
|
||||
}
|
||||
|
||||
/////////
|
||||
/// Event
|
||||
/// TODO: Need to figure out if I can fetch event sets from cache one day (eg, ongoing events)
|
||||
/////////
|
||||
suspend fun getEvent(guildId: Snowflake, calendarNumber: Int, id: String): Event? {
|
||||
var event = eventCache.get(guildId, id)
|
||||
if (event != null) return event
|
||||
|
||||
val calendar = getCalendar(guildId, calendarNumber) ?: return null
|
||||
|
||||
event = calendarProviders
|
||||
.first { it.host == calendar.metadata.host }
|
||||
.getEvent(calendar, id)
|
||||
if (event != null) eventCache.put(guildId, id, event)
|
||||
|
||||
// Since the underlying implementation will emit an error on a non 200/404 condition, we can safely do this
|
||||
if (event == null) announcementService.deleteAnnouncements(guildId, id)
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
suspend fun getUpcomingEvents(guildId: Snowflake, calendarNumber: Int, amount: Int): List<Event> {
|
||||
val calendar = getCalendar(guildId, calendarNumber) ?: return emptyList()
|
||||
|
||||
val events = calendarProviders
|
||||
.first { it.host == calendar.metadata.host }
|
||||
.getUpcomingEvents(calendar, amount)
|
||||
events.forEach { event -> eventCache.put(guildId, event.id, event) }
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
suspend fun getOngoingEvents(guildId: Snowflake, calendarNumber: Int): List<Event> {
|
||||
val calendar = getCalendar(guildId, calendarNumber) ?: return emptyList()
|
||||
|
||||
val events = calendarProviders
|
||||
.first { it.host == calendar.metadata.host }
|
||||
.getOngoingEvents(calendar)
|
||||
events.forEach { event -> eventCache.put(guildId, event.id, event) }
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
suspend fun getEventsInTimeRange(guildId: Snowflake, calendarNumber: Int, start: Instant, end: Instant): List<Event> {
|
||||
val calendar = getCalendar(guildId, calendarNumber) ?: return emptyList()
|
||||
|
||||
val events = calendarProviders
|
||||
.first { it.host == calendar.metadata.host }
|
||||
.getEventsInTimeRange(calendar, start, end)
|
||||
events.forEach { event -> eventCache.put(guildId, event.id, event) }
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
suspend fun getEventsInNext24HourPeriod(guildId: Snowflake, calendarNumber: Int, start: Instant): List<Event> {
|
||||
return getEventsInTimeRange(guildId, calendarNumber, start, start.plus(1, ChronoUnit.DAYS))
|
||||
}
|
||||
|
||||
suspend fun getEventsInMonth(guildId: Snowflake, calendarNumber: Int, start: Instant, daysInMonth: Int): List<Event> {
|
||||
return getEventsInTimeRange(guildId, calendarNumber, start, start.plus(daysInMonth.toLong(), ChronoUnit.DAYS))
|
||||
}
|
||||
|
||||
suspend fun getEventsInNextNDays(guildId: Snowflake, calendarNumber: Int, days: Int): List<Event> {
|
||||
return getEventsInTimeRange(guildId, calendarNumber, Instant.now(), Instant.now().plus(days.toLong(), ChronoUnit.DAYS))
|
||||
}
|
||||
|
||||
suspend fun createEvent(guildId: Snowflake, calendarNumber: Int, spec: Event.CreateSpec): Event {
|
||||
val calendar = getCalendar(guildId, calendarNumber) ?: throw NotFoundException("Cannot create a new event without a calendar")
|
||||
|
||||
val event = calendarProviders
|
||||
.first { it.host == calendar.metadata.host }
|
||||
.createEvent(calendar, spec)
|
||||
|
||||
eventCache.put(guildId, event.id, event)
|
||||
|
||||
// Update static messages in case event is visible
|
||||
staticMessageService.updateStaticMessages(guildId, event.calendarNumber)
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
suspend fun updateEvent(guildId: Snowflake, calendarNumber: Int, spec: Event.UpdateSpec): Event {
|
||||
val calendar = getCalendar(guildId, calendarNumber) ?: throw NotFoundException("Cannot update event without a calendar")
|
||||
|
||||
val event = calendarProviders
|
||||
.first { it.host == calendar.metadata.host }
|
||||
.updateEvent(calendar, spec)
|
||||
|
||||
// This should make sure that any recurring children are removed from cache if this was a recurring parent being updated
|
||||
if (!event.id.contains("_")) {
|
||||
eventCache.getAll(guildId)
|
||||
.filter { it.id.startsWith(event.id) }
|
||||
.forEach { eventCache.evict(guildId, it.id) }
|
||||
}
|
||||
eventCache.put(guildId, event.id, event)
|
||||
|
||||
cancelEventWizard(guildId, event.id)
|
||||
|
||||
// Update static messages in case event is visible
|
||||
staticMessageService.updateStaticMessages(guildId, event.calendarNumber)
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
suspend fun deleteEvent(guildId: Snowflake, calendarNumber: Int, id: String) {
|
||||
val calendar = getCalendar(guildId, calendarNumber) ?: return
|
||||
|
||||
calendarProviders
|
||||
.first { it.host == calendar.metadata.host }
|
||||
.deleteEvent(calendar, id)
|
||||
|
||||
// Make sure if this is a recurring parent, we delete all children from cache
|
||||
if (!id.contains("_")) {
|
||||
eventCache.getAll(guildId)
|
||||
.filter { it.id.startsWith(id) }
|
||||
.forEach { eventCache.evict(guildId, it.id) }
|
||||
}
|
||||
|
||||
eventMetadataService.deleteEventMetadata(guildId, id)
|
||||
announcementService.deleteAnnouncements(guildId, id)
|
||||
staticMessageService.updateStaticMessages(guildId, calendarNumber)
|
||||
|
||||
cancelEventWizard(guildId, id)
|
||||
|
||||
}
|
||||
|
||||
|
||||
/////////
|
||||
/// Wizards
|
||||
/////////
|
||||
suspend fun getCalendarWizard(guildId: Snowflake, userId: Snowflake): CalendarWizardState? {
|
||||
return calendarWizardStateCache.get(guildId, userId)
|
||||
}
|
||||
|
||||
suspend fun putCalendarWizard(state: CalendarWizardState) {
|
||||
calendarWizardStateCache.put(state.guildId, state.userId, state)
|
||||
}
|
||||
|
||||
suspend fun cancelCalendarWizard(guildId: Snowflake, userId: Snowflake) {
|
||||
calendarWizardStateCache.evict(guildId, userId)
|
||||
}
|
||||
|
||||
suspend fun cancelCalendarWizard(guildId: Snowflake, calendarNumber: Int) {
|
||||
calendarWizardStateCache.getAll(guildId)
|
||||
.filter { it.entity.metadata.number == calendarNumber }
|
||||
.forEach { calendarWizardStateCache.evict(guildId, it.userId) }
|
||||
}
|
||||
|
||||
suspend fun getEventWizard(guildId: Snowflake, userId: Snowflake): EventWizardState? {
|
||||
return eventWizardStateCache.get(guildId, userId)
|
||||
}
|
||||
|
||||
suspend fun putEventWizard(state: EventWizardState) {
|
||||
eventWizardStateCache.put(state.guildId, state.userId, state)
|
||||
}
|
||||
|
||||
suspend fun cancelEventWizard(guildId: Snowflake, userId: Snowflake) {
|
||||
eventWizardStateCache.evict(guildId, userId)
|
||||
}
|
||||
|
||||
suspend fun cancelEventWizard(guildId: Snowflake, calendarNumber: Int) {
|
||||
eventWizardStateCache.getAll(guildId)
|
||||
.filter { it.entity.calendarNumber == calendarNumber }
|
||||
.forEach { eventWizardStateCache.evict(guildId, it.userId) }
|
||||
}
|
||||
|
||||
suspend fun cancelEventWizard(guildId: Snowflake, eventId: String) {
|
||||
eventWizardStateCache.getAll(guildId)
|
||||
.filter { it.entity.id == eventId }
|
||||
.forEach { eventWizardStateCache.evict(guildId, it.userId) }
|
||||
}
|
||||
|
||||
|
||||
/////////
|
||||
/// Extra functions
|
||||
/////////
|
||||
suspend fun canAddNewCalendar(guildId: Snowflake): Boolean {
|
||||
val calCount = getCalendarCount(guildId)
|
||||
if (calCount == 0L) return true
|
||||
|
||||
val settings = settingsService.getSettings(guildId)
|
||||
return calCount < settings.maxCalendars
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,21 +4,14 @@ import discord4j.common.util.Snowflake
|
||||
import discord4j.core.DiscordClient
|
||||
import discord4j.core.spec.EmbedCreateSpec
|
||||
import kotlinx.coroutines.reactor.awaitSingle
|
||||
import org.dreamexposure.discal.Application
|
||||
import org.dreamexposure.discal.GitProperty
|
||||
import org.dreamexposure.discal.*
|
||||
import org.dreamexposure.discal.core.config.Config
|
||||
import org.dreamexposure.discal.core.entities.Calendar
|
||||
import org.dreamexposure.discal.core.entities.Event
|
||||
import org.dreamexposure.discal.core.enums.announcement.AnnouncementStyle
|
||||
import org.dreamexposure.discal.core.enums.event.EventColor
|
||||
import org.dreamexposure.discal.core.enums.time.DiscordTimestampFormat
|
||||
import org.dreamexposure.discal.core.enums.time.DiscordTimestampFormat.LONG_DATETIME
|
||||
import org.dreamexposure.discal.core.extensions.*
|
||||
import org.dreamexposure.discal.core.extensions.discord4j.getCalendar
|
||||
import org.dreamexposure.discal.core.extensions.discord4j.getSettings
|
||||
import org.dreamexposure.discal.core.`object`.GuildSettings
|
||||
import org.dreamexposure.discal.core.`object`.new.Announcement
|
||||
import org.dreamexposure.discal.core.`object`.new.Rsvp
|
||||
import org.dreamexposure.discal.core.`object`.new.WizardState
|
||||
import org.dreamexposure.discal.core.`object`.new.*
|
||||
import org.dreamexposure.discal.core.`object`.new.GuildSettings.AnnouncementStyle.*
|
||||
import org.dreamexposure.discal.core.utils.GlobalVal
|
||||
import org.dreamexposure.discal.core.utils.getCommonMsg
|
||||
import org.dreamexposure.discal.core.utils.getEmbedMessage
|
||||
@@ -32,6 +25,7 @@ import java.time.temporal.ChronoUnit
|
||||
|
||||
@Component
|
||||
class EmbedService(
|
||||
private val settingsService: GuildSettingsService,
|
||||
private val beanFactory: BeanFactory,
|
||||
) {
|
||||
private val discordClient: DiscordClient
|
||||
@@ -39,15 +33,15 @@ class EmbedService(
|
||||
|
||||
|
||||
private suspend fun defaultEmbedBuilder(settings: GuildSettings): EmbedCreateSpec.Builder {
|
||||
val guild = discordClient.getGuildById(settings.guildID).data.awaitSingle()
|
||||
val guild = discordClient.getGuildById(settings.guildId).data.awaitSingle()
|
||||
|
||||
val iconUrl = if (settings.branded && guild.icon().isPresent)
|
||||
"${GlobalVal.discordCdnUrl}/icons/${settings.guildID.asString()}/${guild.icon().get()}.png"
|
||||
val iconUrl = if (settings.interfaceStyle.branded && guild.icon().isPresent)
|
||||
"${GlobalVal.discordCdnUrl}/icons/${settings.guildId.asString()}/${guild.icon().get()}.png"
|
||||
else GlobalVal.iconUrl
|
||||
|
||||
return EmbedCreateSpec.builder()
|
||||
.author(
|
||||
if (settings.branded) guild.name() else getCommonMsg("bot.name", settings),
|
||||
if (settings.interfaceStyle.branded) guild.name() else getCommonMsg("bot.name", settings.locale),
|
||||
Config.URL_BASE.getString(),
|
||||
iconUrl
|
||||
)
|
||||
@@ -56,47 +50,64 @@ class EmbedService(
|
||||
////////////////////////////
|
||||
////// General Embeds //////
|
||||
////////////////////////////
|
||||
suspend fun discalInfoEmbed(settings: GuildSettings, calendarCount: Long, announcementCount: Long): EmbedCreateSpec {
|
||||
val guildCount = discordClient.guilds.count().awaitSingle()
|
||||
|
||||
suspend fun discalInfoEmbed(settings: GuildSettings, guildCount: Long, calendarCount: Long, announcementCount: Long): EmbedCreateSpec {
|
||||
return defaultEmbedBuilder(settings)
|
||||
.color(GlobalVal.discalColor)
|
||||
.title(getEmbedMessage("discal", "info.title", settings))
|
||||
.addField(getEmbedMessage("discal", "info.field.version", settings), GitProperty.DISCAL_VERSION.value, false)
|
||||
.addField(getEmbedMessage("discal", "info.field.library", settings), "Discord4J ${GitProperty.DISCAL_VERSION_D4J.value}", false)
|
||||
.addField(getEmbedMessage("discal", "info.field.shard", settings), "${Application.getShardIndex()}/${Application.getShardCount()}", true)
|
||||
.addField(getEmbedMessage("discal", "info.field.guilds", settings), "$guildCount", true)
|
||||
.title(getEmbedMessage("discal", "info.title", settings.locale))
|
||||
.addField(getEmbedMessage("discal", "info.field.version", settings.locale), GitProperty.DISCAL_VERSION.value, false)
|
||||
.addField(getEmbedMessage("discal", "info.field.library", settings.locale), "Discord4J ${GitProperty.DISCAL_VERSION_D4J.value}", false)
|
||||
.addField(getEmbedMessage("discal", "info.field.shard", settings.locale), "${Application.getShardIndex()}/${Application.getShardCount()}", true)
|
||||
.addField(getEmbedMessage("discal", "info.field.guilds", settings.locale), "$guildCount", true)
|
||||
.addField(
|
||||
getEmbedMessage("discal", "info.field.uptime", settings),
|
||||
getEmbedMessage("discal", "info.field.uptime", settings.locale),
|
||||
Application.getUptime().getHumanReadable(),
|
||||
false
|
||||
).addField(getEmbedMessage("discal", "info.field.calendars", settings), "$calendarCount", true)
|
||||
.addField(getEmbedMessage("discal", "info.field.announcements", settings), "$announcementCount", true)
|
||||
.addField(getEmbedMessage("discal", "info.field.links", settings),
|
||||
).addField(getEmbedMessage("discal", "info.field.calendars", settings.locale), "$calendarCount", true)
|
||||
.addField(getEmbedMessage("discal", "info.field.announcements", settings.locale), "$announcementCount", true)
|
||||
.addField(getEmbedMessage("discal", "info.field.links", settings.locale),
|
||||
getEmbedMessage("discal",
|
||||
"info.field.links.value",
|
||||
settings,
|
||||
settings.locale,
|
||||
"${Config.URL_BASE.getString()}/commands",
|
||||
Config.URL_SUPPORT.getString(),
|
||||
Config.URL_INVITE.getString(),
|
||||
"https://www.patreon.com/Novafox"
|
||||
),
|
||||
false
|
||||
).footer(getEmbedMessage("discal", "info.footer", settings), null)
|
||||
).footer(getEmbedMessage("discal", "info.footer", settings.locale), null)
|
||||
.build()
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
////// Settings Embeds //////
|
||||
/////////////////////////////
|
||||
suspend fun settingsEmbeds(settings: GuildSettings): EmbedCreateSpec {
|
||||
val controlRoleValue = if (settings.controlRole == null) "<@&${settings.guildId.asLong()}>" else "<@&${settings.controlRole}>"
|
||||
|
||||
return defaultEmbedBuilder(settings)
|
||||
.title(getEmbedMessage("settings", "view.title", settings.locale))
|
||||
.addField(getEmbedMessage("settings", "view.field.role", settings.locale), controlRoleValue, false)
|
||||
.addField(getEmbedMessage("settings", "view.field.style", settings.locale), settings.interfaceStyle.announcementStyle.name, true)
|
||||
.addField(getEmbedMessage("settings", "view.field.format", settings.locale), settings.interfaceStyle.timeFormat.name, true)
|
||||
.addField(getEmbedMessage("settings", "view.field.eventKeepDuration", settings.locale), "${settings.eventKeepDuration}", true)
|
||||
.addField(getEmbedMessage("settings", "view.field.lang", settings.locale), settings.locale.displayName, false)
|
||||
.addField(getEmbedMessage("settings", "view.field.patron", settings.locale), "${settings.patronGuild}", true)
|
||||
.addField(getEmbedMessage("settings", "view.field.dev", settings.locale), "${settings.devGuild}", true)
|
||||
.addField(getEmbedMessage("settings", "view.field.cal", settings.locale), "${settings.maxCalendars}", true)
|
||||
.addField(getEmbedMessage("settings", "view.field.brand", settings.locale), "${settings.interfaceStyle.branded}", false)
|
||||
.footer(getEmbedMessage("settings", "view.footer", settings.locale), null)
|
||||
.build()
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
////// Calendar Embeds //////
|
||||
/////////////////////////////
|
||||
suspend fun calendarOverviewEmbed(calendar: Calendar, settings: GuildSettings, showUpdate: Boolean): EmbedCreateSpec {
|
||||
suspend fun calendarOverviewEmbed(calendar: Calendar, events: List<Event>, showUpdate: Boolean): EmbedCreateSpec {
|
||||
val settings = settingsService.getSettings(calendar.metadata.guildId)
|
||||
val builder = defaultEmbedBuilder(settings)
|
||||
|
||||
// Get the events to build the overview
|
||||
val events = calendar.getUpcomingEvents(15)
|
||||
.collectList()
|
||||
.map { it.groupByDate() }
|
||||
.awaitSingle()
|
||||
// Get events sorted and grouped
|
||||
val groupedEvents = events.groupByDate()
|
||||
|
||||
//Handle optional fields
|
||||
if (calendar.name.isNotBlank())
|
||||
@@ -106,7 +117,7 @@ class EmbedService(
|
||||
|
||||
// Truncate dates to 23 due to discord enforcing the field limit
|
||||
val truncatedEvents = mutableMapOf<ZonedDateTime, List<Event>>()
|
||||
for (event in events) {
|
||||
for (event in groupedEvents) {
|
||||
if (truncatedEvents.size < 23) {
|
||||
truncatedEvents[event.key] = event.value
|
||||
} else break
|
||||
@@ -114,7 +125,7 @@ class EmbedService(
|
||||
|
||||
// Show events
|
||||
truncatedEvents.forEach { date ->
|
||||
val title = date.key.toInstant().humanReadableDate(calendar.timezone, settings.timeFormat, longDay = true)
|
||||
val title = date.key.toInstant().humanReadableDate(calendar.timezone, settings.interfaceStyle.timeFormat, longDay = true)
|
||||
|
||||
// sort events
|
||||
val sortedEvents = date.value.sortedBy { it.start }
|
||||
@@ -126,32 +137,32 @@ class EmbedService(
|
||||
content.append("```\n")
|
||||
|
||||
// determine time length
|
||||
val timeDisplayLen = ("${it.start.humanReadableTime(it.timezone, settings.timeFormat)} -" +
|
||||
" ${it.end.humanReadableTime(it.timezone, settings.timeFormat)} ").length
|
||||
val timeDisplayLen = ("${it.start.humanReadableTime(it.timezone, settings.interfaceStyle.timeFormat)} -" +
|
||||
" ${it.end.humanReadableTime(it.timezone, settings.interfaceStyle.timeFormat)} ").length
|
||||
|
||||
// Displaying time
|
||||
if (it.isAllDay()) {
|
||||
content.append(getCommonMsg("generic.time.allDay", settings).padCenter(timeDisplayLen))
|
||||
content.append(getCommonMsg("generic.time.allDay", settings.locale).padCenter(timeDisplayLen))
|
||||
.append("| ")
|
||||
} else {
|
||||
// Add start text
|
||||
var str = if (it.start.isBefore(date.key.toInstant())) {
|
||||
"${getCommonMsg("generic.time.continued", settings)} - "
|
||||
"${getCommonMsg("generic.time.continued", settings.locale)} - "
|
||||
} else {
|
||||
"${it.start.humanReadableTime(it.timezone, settings.timeFormat)} - "
|
||||
"${it.start.humanReadableTime(it.timezone, settings.interfaceStyle.timeFormat)} - "
|
||||
}
|
||||
// Add end text
|
||||
str += if (it.end.isAfter(date.key.toInstant().plus(1, ChronoUnit.DAYS))) {
|
||||
getCommonMsg("generic.time.continued", settings)
|
||||
getCommonMsg("generic.time.continued", settings.locale)
|
||||
} else {
|
||||
"${it.end.humanReadableTime(it.timezone, settings.timeFormat)} "
|
||||
"${it.end.humanReadableTime(it.timezone, settings.interfaceStyle.timeFormat)} "
|
||||
}
|
||||
content.append(str.padCenter(timeDisplayLen))
|
||||
.append("| ")
|
||||
}
|
||||
// Display name or ID if not set
|
||||
if (it.name.isNotBlank()) content.append(it.name)
|
||||
else content.append(getEmbedMessage("calendar", "link.field.id", settings)).append(" ${it.eventId}")
|
||||
else content.append(getEmbedMessage("calendar", "link.field.id", settings.locale)).append(" ${it.id}")
|
||||
content.append("\n")
|
||||
if (it.location.isNotBlank()) content.append(" Location: ")
|
||||
.append(it.location.embedFieldSafe())
|
||||
@@ -168,25 +179,25 @@ class EmbedService(
|
||||
// set footer
|
||||
if (showUpdate) {
|
||||
val lastUpdate = Instant.now().asDiscordTimestamp(DiscordTimestampFormat.RELATIVE_TIME)
|
||||
builder.footer(getEmbedMessage("calendar", "link.footer.update", settings, lastUpdate), null)
|
||||
builder.footer(getEmbedMessage("calendar", "link.footer.update", settings.locale, lastUpdate), null)
|
||||
.timestamp(Instant.now())
|
||||
} else builder.footer(getEmbedMessage("calendar", "link.footer.default", settings), null)
|
||||
} else builder.footer(getEmbedMessage("calendar", "link.footer.default", settings.locale), null)
|
||||
|
||||
// finish and return
|
||||
return builder.addField(getEmbedMessage("calendar", "link.field.timezone", settings), calendar.zoneName, true)
|
||||
.addField(getEmbedMessage("calendar", "link.field.number", settings), "${calendar.calendarNumber}", true)
|
||||
return builder.addField(getEmbedMessage("calendar", "link.field.timezone", settings.locale), calendar.timezone.id, true)
|
||||
.addField(getEmbedMessage("calendar", "link.field.number", settings.locale), "${calendar.metadata.number}", true)
|
||||
.url(calendar.link)
|
||||
.color(GlobalVal.discalColor)
|
||||
.build()
|
||||
}
|
||||
|
||||
suspend fun linkCalendarEmbed(calendarNumber: Int, settings: GuildSettings, overview: Boolean): EmbedCreateSpec {
|
||||
val calendar = discordClient.getGuildById(settings.guildID).getCalendar(calendarNumber).awaitSingle()
|
||||
return if (overview) calendarOverviewEmbed(calendar, settings, showUpdate = false)
|
||||
else linkCalendarEmbed(calendar, settings)
|
||||
suspend fun linkCalendarEmbed(calendar: Calendar, events: List<Event>?): EmbedCreateSpec {
|
||||
return if (events != null) calendarOverviewEmbed(calendar, events, showUpdate = false)
|
||||
else linkCalendarEmbed(calendar)
|
||||
}
|
||||
|
||||
suspend fun linkCalendarEmbed(calendar: Calendar, settings: GuildSettings): EmbedCreateSpec {
|
||||
suspend fun linkCalendarEmbed(calendar: Calendar): EmbedCreateSpec {
|
||||
val settings = settingsService.getSettings(calendar.metadata.guildId)
|
||||
val builder = defaultEmbedBuilder(settings)
|
||||
|
||||
//Handle optional fields
|
||||
@@ -195,41 +206,235 @@ class EmbedService(
|
||||
if (calendar.description.isNotBlank())
|
||||
builder.description(calendar.description.toMarkdown().embedDescriptionSafe())
|
||||
|
||||
return builder.addField(getEmbedMessage("calendar", "link.field.timezone", settings), calendar.zoneName, false)
|
||||
.addField(getEmbedMessage("calendar", "link.field.host", settings), calendar.calendarData.host.name, true)
|
||||
.addField(getEmbedMessage("calendar", "link.field.number", settings), "${calendar.calendarNumber}", true)
|
||||
.addField(getEmbedMessage("calendar", "link.field.id", settings), calendar.calendarId, false)
|
||||
return builder.addField(getEmbedMessage("calendar", "link.field.timezone", settings.locale), calendar.timezone.id, false)
|
||||
.addField(getEmbedMessage("calendar", "link.field.host", settings.locale), calendar.metadata.host.name, true)
|
||||
.addField(getEmbedMessage("calendar", "link.field.number", settings.locale), "${calendar.metadata.number}", true)
|
||||
.addField(getEmbedMessage("calendar", "link.field.id", settings.locale), calendar.metadata.id, false)
|
||||
.url(calendar.link)
|
||||
.footer(getEmbedMessage("calendar", "link.footer.default", settings), null)
|
||||
.footer(getEmbedMessage("calendar", "link.footer.default", settings.locale), null)
|
||||
.color(GlobalVal.discalColor)
|
||||
.build()
|
||||
}
|
||||
|
||||
suspend fun calendarTimeEmbed(calendar: Calendar, settings: GuildSettings): EmbedCreateSpec {
|
||||
val formattedTime = Instant.now().humanReadableFullSimple(calendar.timezone, settings.timeFormat)
|
||||
val formattedTime = Instant.now().humanReadableFullSimple(calendar.timezone, settings.interfaceStyle.timeFormat)
|
||||
val formattedLocal = Instant.now().asDiscordTimestamp(DiscordTimestampFormat.SHORT_DATETIME)
|
||||
|
||||
return defaultEmbedBuilder(settings)
|
||||
.title(getEmbedMessage("time", "embed.title", settings))
|
||||
.addField(getEmbedMessage("time", "embed.field.current", settings), formattedTime, true)
|
||||
.addField(getEmbedMessage("time", "embed.field.timezone", settings), calendar.zoneName, true)
|
||||
.addField(getEmbedMessage("time", "embed.field.local", settings), formattedLocal, false)
|
||||
.footer(getEmbedMessage("time", "embed.footer", settings), null)
|
||||
.title(getEmbedMessage("time", "embed.title", settings.locale))
|
||||
.addField(getEmbedMessage("time", "embed.field.current", settings.locale), formattedTime, true)
|
||||
.addField(getEmbedMessage("time", "embed.field.timezone", settings.locale), calendar.timezone.id, true)
|
||||
.addField(getEmbedMessage("time", "embed.field.local", settings.locale), formattedLocal, false)
|
||||
.footer(getEmbedMessage("time", "embed.footer", settings.locale), null)
|
||||
.url(calendar.link)
|
||||
.color(GlobalVal.discalColor)
|
||||
.build()
|
||||
}
|
||||
|
||||
suspend fun calendarWizardEmbed(wizard: CalendarWizardState, settings: GuildSettings): EmbedCreateSpec {
|
||||
val builder = defaultEmbedBuilder(settings)
|
||||
.title(getEmbedMessage("calendar", "wizard.title", settings.locale))
|
||||
.footer(getEmbedMessage("calendar", "wizard.footer", settings.locale), null)
|
||||
.addField(
|
||||
getEmbedMessage("calendar", "wizard.field.name", settings.locale),
|
||||
wizard.entity.name.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
).addField(
|
||||
getEmbedMessage("calendar", "wizard.field.description", settings.locale),
|
||||
wizard.entity.description.ifEmpty { getCommonMsg("embed.unset", settings.locale) }.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
).addField(
|
||||
getEmbedMessage("calendar", "wizard.field.timezone", settings.locale),
|
||||
wizard.entity.timezone.id,
|
||||
true
|
||||
).addField(
|
||||
getEmbedMessage("calendar", "wizard.field.host", settings.locale),
|
||||
wizard.entity.metadata.host.name,
|
||||
true
|
||||
)
|
||||
|
||||
if (wizard.editing) builder.addField(
|
||||
getEmbedMessage("calendar", "wizard.field.id", settings.locale),
|
||||
wizard.entity.metadata.id,
|
||||
false
|
||||
)
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
//////////////////////////
|
||||
////// Event Embeds //////
|
||||
//////////////////////////
|
||||
suspend fun fullEventEmbed(event: Event, settings: GuildSettings): EmbedCreateSpec {
|
||||
val builder = defaultEmbedBuilder(settings)
|
||||
.footer(getEmbedMessage("event", "full.footer", settings.locale, event.id), null)
|
||||
.color(event.color.asColor())
|
||||
|
||||
if (event.name.isNotBlank())
|
||||
builder.title(event.name.toMarkdown().embedTitleSafe())
|
||||
if (event.description.isNotBlank())
|
||||
builder.description(event.description.toMarkdown().embedDescriptionSafe())
|
||||
|
||||
builder.addField(
|
||||
getEmbedMessage("event", "full.field.start", settings.locale),
|
||||
event.start.asDiscordTimestamp(LONG_DATETIME),
|
||||
true)
|
||||
builder.addField(
|
||||
getEmbedMessage("event", "full.field.end", settings.locale),
|
||||
event.end.asDiscordTimestamp(LONG_DATETIME),
|
||||
true
|
||||
)
|
||||
|
||||
if (event.location.isNotBlank()) builder.addField(
|
||||
getEmbedMessage("event", "full.field.location", settings.locale),
|
||||
event.location.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
)
|
||||
|
||||
builder.addField(getEmbedMessage("event", "full.field.cal", settings.locale), "${event.calendarNumber}", false)
|
||||
|
||||
if (event.image.isNotEmpty())
|
||||
builder.image(event.image)
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
suspend fun condensedEventEmbed(event: Event, settings: GuildSettings): EmbedCreateSpec {
|
||||
val builder = defaultEmbedBuilder(settings)
|
||||
.footer(getEmbedMessage("event", "con.footer", settings.locale, event.id), null)
|
||||
.color(event.color.asColor())
|
||||
|
||||
if (event.name.isNotBlank())
|
||||
builder.title(event.name.toMarkdown().embedTitleSafe())
|
||||
|
||||
builder.addField(
|
||||
getEmbedMessage("event", "con.field.start", settings.locale),
|
||||
event.start.asDiscordTimestamp(LONG_DATETIME),
|
||||
true
|
||||
)
|
||||
|
||||
if (event.location.isNotBlank()) builder.addField(
|
||||
getEmbedMessage("event", "con.field.location", settings.locale),
|
||||
event.location.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
)
|
||||
|
||||
if (event.image.isNotBlank())
|
||||
builder.thumbnail(event.image)
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
suspend fun eventWizardEmbed(wizard: EventWizardState, settings: GuildSettings): EmbedCreateSpec {
|
||||
val builder = defaultEmbedBuilder(settings)
|
||||
.title(getEmbedMessage("event", "wizard.title", settings.locale))
|
||||
.footer(getEmbedMessage("event", "wizard.footer", settings.locale), null)
|
||||
.color(wizard.entity.color.asColor())
|
||||
|
||||
if (!wizard.entity.name.isNullOrBlank()) builder.addField(
|
||||
getEmbedMessage("event", "wizard.field.name", settings.locale),
|
||||
wizard.entity.name.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
) else builder.addField(
|
||||
getEmbedMessage("event", "wizard.field.name", settings.locale),
|
||||
getCommonMsg("embed.unset", settings.locale),
|
||||
false
|
||||
)
|
||||
|
||||
if (!wizard.entity.description.isNullOrBlank()) builder.addField(
|
||||
getEmbedMessage("event", "wizard.field.desc", settings.locale),
|
||||
wizard.entity.description.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
) else builder.addField(
|
||||
getEmbedMessage("event", "wizard.field.desc", settings.locale),
|
||||
getCommonMsg("embed.unset", settings.locale),
|
||||
false
|
||||
)
|
||||
|
||||
if (!wizard.entity.location.isNullOrBlank()) builder.addField(
|
||||
getEmbedMessage("event", "wizard.field.location", settings.locale),
|
||||
wizard.entity.location.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
) else builder.addField(
|
||||
getEmbedMessage("event", "wizard.field.location", settings.locale),
|
||||
getCommonMsg("embed.unset", settings.locale),
|
||||
false
|
||||
)
|
||||
|
||||
if (wizard.entity.start != null) builder.addField(
|
||||
getEmbedMessage("event", "wizard.field.start", settings.locale),
|
||||
wizard.entity.start.humanReadableFull(wizard.entity.timezone, settings.interfaceStyle.timeFormat),
|
||||
true
|
||||
) else builder.addField(
|
||||
getEmbedMessage("event", "wizard.field.start", settings.locale),
|
||||
getCommonMsg("embed.unset", settings.locale),
|
||||
true
|
||||
)
|
||||
|
||||
if (wizard.entity.end != null) builder.addField(
|
||||
getEmbedMessage("event", "wizard.field.end", settings.locale),
|
||||
wizard.entity.end.humanReadableFull(wizard.entity.timezone, settings.interfaceStyle.timeFormat),
|
||||
true
|
||||
) else builder.addField(
|
||||
getEmbedMessage("event", "wizard.field.end", settings.locale),
|
||||
getCommonMsg("embed.unset", settings.locale),
|
||||
true
|
||||
)
|
||||
|
||||
if (wizard.entity.recurrence != null) builder.addField(
|
||||
getEmbedMessage("event", "wizard.field.recurrence", settings.locale),
|
||||
wizard.entity.recurrence.toHumanReadable(),
|
||||
true
|
||||
) else if (wizard.editing && wizard.entity.id != null && wizard.entity.id.contains("_")) builder.addField(
|
||||
getEmbedMessage("event", "wizard.field.recurrence", settings.locale),
|
||||
getEmbedMessage("event", "wizard.field.recurrence.child", settings.locale, wizard.entity.id.split("_")[0]),
|
||||
false,
|
||||
) else builder.addField(
|
||||
getEmbedMessage("event", "wizard.field.recurrence", settings.locale),
|
||||
getCommonMsg("embed.unset", settings.locale),
|
||||
true
|
||||
)
|
||||
|
||||
builder.addField(getEmbedMessage("event", "wizard.field.timezone", settings.locale), wizard.entity.timezone.id, false)
|
||||
|
||||
if (wizard.editing)
|
||||
builder.addField(getEmbedMessage("event", "wizard.field.id", settings.locale), wizard.entity.id!!, true)
|
||||
builder.addField(getEmbedMessage("event", "wizard.field.calendar", settings.locale), wizard.entity.calendarNumber.toString(), true)
|
||||
|
||||
if (wizard.entity.image != null)
|
||||
builder.image(wizard.entity.image)
|
||||
|
||||
// Handle displaying warnings
|
||||
val warnings = mutableListOf<String>()
|
||||
|
||||
if (wizard.entity.name.isNullOrBlank()) {
|
||||
warnings.add(getEmbedMessage("event", "warning.wizard.noName", settings.locale))
|
||||
}
|
||||
// Checking end time is not needed
|
||||
if (wizard.entity.start != null && wizard.entity.start.isBefore(Instant.now())) {
|
||||
warnings.add(getEmbedMessage("event", "warning.wizard.past", settings.locale))
|
||||
}
|
||||
if (wizard.entity.start != null && wizard.entity.end != null) {
|
||||
if (Duration.between(wizard.entity.start, wizard.entity.end).toDays() > 30) {
|
||||
warnings.add(getEmbedMessage("event", "warning.wizard.veryLong", settings.locale))
|
||||
}
|
||||
|
||||
}
|
||||
if (warnings.isNotEmpty()) {
|
||||
val warnText = "```fix\n${warnings.joinToString("\n")}\n```"
|
||||
builder.addField(getEmbedMessage("event", "wizard.field.warnings", settings.locale), warnText, false)
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
////// RSVP Embeds //////
|
||||
/////////////////////////
|
||||
suspend fun rsvpDmFollowupEmbed(rsvp: Rsvp, userId: Snowflake): EmbedCreateSpec {
|
||||
// TODO: These will be replaced by service calls eventually as I migrate components over to new patterns
|
||||
suspend fun rsvpDmFollowupEmbed(rsvp: Rsvp, event: Event, userId: Snowflake): EmbedCreateSpec {
|
||||
val restGuild = discordClient.getGuildById(rsvp.guildId)
|
||||
val guildData = restGuild.data.awaitSingle()
|
||||
val guildSettings = restGuild.getSettings().awaitSingle()
|
||||
val event = restGuild.getCalendar(rsvp.calendarNumber)
|
||||
.flatMap { it.getEvent(rsvp.eventId) }.awaitSingle()
|
||||
val settings = settingsService.getSettings(rsvp.guildId)
|
||||
|
||||
|
||||
val iconUrl = if (guildData.icon().isPresent)
|
||||
@@ -239,20 +444,20 @@ class EmbedService(
|
||||
val builder = EmbedCreateSpec.builder()
|
||||
// Even without branding enabled, we want the user to know what guild this is because it's in DMs
|
||||
.author(guildData.name(), Config.URL_BASE.getString(), iconUrl)
|
||||
.title(getEmbedMessage("rsvp", "waitlist.title", guildSettings))
|
||||
.description(getEmbedMessage("rsvp", "waitlist.desc", guildSettings, userId.asString(), event.name))
|
||||
.title(getEmbedMessage("rsvp", "waitlist.title", settings.locale))
|
||||
.description(getEmbedMessage("rsvp", "waitlist.desc", settings.locale, userId.asString(), event.name))
|
||||
.addField(
|
||||
getEmbedMessage("rsvp", "waitlist.field.start", guildSettings),
|
||||
event.start.asDiscordTimestamp(DiscordTimestampFormat.LONG_DATETIME),
|
||||
getEmbedMessage("rsvp", "waitlist.field.start", settings.locale),
|
||||
event.start.asDiscordTimestamp(LONG_DATETIME),
|
||||
true
|
||||
).addField(
|
||||
getEmbedMessage("rsvp", "waitlist.field.end", guildSettings),
|
||||
event.end.asDiscordTimestamp(DiscordTimestampFormat.LONG_DATETIME),
|
||||
getEmbedMessage("rsvp", "waitlist.field.end", settings.locale),
|
||||
event.end.asDiscordTimestamp(LONG_DATETIME),
|
||||
true
|
||||
).footer(getEmbedMessage("rsvp", "waitlist.footer", guildSettings, event.eventId), null)
|
||||
).footer(getEmbedMessage("rsvp", "waitlist.footer", settings.locale, event.id), null)
|
||||
|
||||
if (event.location.isNotBlank()) builder.addField(
|
||||
getEmbedMessage("rsvp", "waitlist.field.location", guildSettings),
|
||||
getEmbedMessage("rsvp", "waitlist.field.location", settings.locale),
|
||||
event.location.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
)
|
||||
@@ -307,87 +512,88 @@ class EmbedService(
|
||||
}
|
||||
|
||||
val limitValue = if (rsvp.limit < 0) {
|
||||
getEmbedMessage("rsvp", "list.field.limit.value", settings, "${rsvp.getCurrentCount()}")
|
||||
getEmbedMessage("rsvp", "list.field.limit.value", settings.locale, "${rsvp.getCurrentCount()}")
|
||||
} else "${rsvp.getCurrentCount()}/${rsvp.limit}"
|
||||
|
||||
|
||||
|
||||
return defaultEmbedBuilder(settings)
|
||||
.color(event.color.asColor())
|
||||
.title(getEmbedMessage("rsvp", "list.title", settings))
|
||||
.addField(getEmbedMessage("rsvp", "list.field.event", settings), rsvp.eventId, false)
|
||||
.addField(getEmbedMessage("rsvp", "list.field.limit", settings), limitValue, true)
|
||||
.addField(getEmbedMessage("rsvp", "list.field.role", settings), role, true)
|
||||
.addField(getEmbedMessage("rsvp", "list.field.onTime", settings), goingOnTime, false)
|
||||
.addField(getEmbedMessage("rsvp", "list.field.late", settings), late, false)
|
||||
.addField(getEmbedMessage("rsvp", "list.field.unsure", settings), undecided, false)
|
||||
.addField(getEmbedMessage("rsvp", "list.field.notGoing", settings), notGoing, false)
|
||||
.addField(getEmbedMessage("rsvp", "list.field.waitList", settings), waitList, false)
|
||||
.footer(getEmbedMessage("rsvp", "list.footer", settings), null)
|
||||
.title(getEmbedMessage("rsvp", "list.title", settings.locale))
|
||||
.addField(getEmbedMessage("rsvp", "list.field.event", settings.locale), rsvp.eventId, false)
|
||||
.addField(getEmbedMessage("rsvp", "list.field.limit", settings.locale), limitValue, true)
|
||||
.addField(getEmbedMessage("rsvp", "list.field.role", settings.locale), role, true)
|
||||
.addField(getEmbedMessage("rsvp", "list.field.onTime", settings.locale), goingOnTime, false)
|
||||
.addField(getEmbedMessage("rsvp", "list.field.late", settings.locale), late, false)
|
||||
.addField(getEmbedMessage("rsvp", "list.field.unsure", settings.locale), undecided, false)
|
||||
.addField(getEmbedMessage("rsvp", "list.field.notGoing", settings.locale), notGoing, false)
|
||||
.addField(getEmbedMessage("rsvp", "list.field.waitList", settings.locale), waitList, false)
|
||||
.footer(getEmbedMessage("rsvp", "list.footer", settings.locale), null)
|
||||
.build()
|
||||
}
|
||||
|
||||
/////////////////////////////////
|
||||
////// Announcement Embeds //////
|
||||
/////////////////////////////////
|
||||
suspend fun determineAnnouncementEmbed(announcement: Announcement, event: Event, settings: GuildSettings): EmbedCreateSpec {
|
||||
return when (settings.announcementStyle) {
|
||||
AnnouncementStyle.FULL -> fullAnnouncementEmbed(announcement, event, settings)
|
||||
AnnouncementStyle.SIMPLE -> simpleAnnouncementEmbed(announcement, event, settings)
|
||||
AnnouncementStyle.EVENT -> eventAnnouncementEmbed(announcement, event, settings)
|
||||
suspend fun determineAnnouncementEmbed(announcement: Announcement, event: Event): EmbedCreateSpec {
|
||||
val settings = settingsService.getSettings(announcement.guildId)
|
||||
return when (settings.interfaceStyle.announcementStyle) {
|
||||
FULL -> fullAnnouncementEmbed(announcement, event, settings)
|
||||
SIMPLE -> simpleAnnouncementEmbed(announcement, event, settings)
|
||||
EVENT -> eventAnnouncementEmbed(announcement, event, settings)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fullAnnouncementEmbed(announcement: Announcement, event: Event, settings: GuildSettings): EmbedCreateSpec {
|
||||
val builder = defaultEmbedBuilder(settings)
|
||||
.color(event.color.asColor())
|
||||
.title(getEmbedMessage("announcement", "full.title", settings))
|
||||
.title(getEmbedMessage("announcement", "full.title", settings.locale))
|
||||
|
||||
if (event.name.isNotBlank()) builder.addField(
|
||||
getEmbedMessage("announcement", "full.field.name", settings),
|
||||
getEmbedMessage("announcement", "full.field.name", settings.locale),
|
||||
event.name.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
)
|
||||
if (event.description.isNotBlank()) builder.addField(
|
||||
getEmbedMessage("announcement", "full.field.desc", settings),
|
||||
getEmbedMessage("announcement", "full.field.desc", settings.locale),
|
||||
event.description.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
)
|
||||
|
||||
builder.addField(
|
||||
getEmbedMessage("announcement", "full.field.start", settings),
|
||||
event.start.asDiscordTimestamp(DiscordTimestampFormat.LONG_DATETIME),
|
||||
getEmbedMessage("announcement", "full.field.start", settings.locale),
|
||||
event.start.asDiscordTimestamp(LONG_DATETIME),
|
||||
true
|
||||
)
|
||||
builder.addField(
|
||||
getEmbedMessage("announcement", "full.field.end", settings),
|
||||
event.end.asDiscordTimestamp(DiscordTimestampFormat.LONG_DATETIME),
|
||||
getEmbedMessage("announcement", "full.field.end", settings.locale),
|
||||
event.end.asDiscordTimestamp(LONG_DATETIME),
|
||||
true
|
||||
)
|
||||
|
||||
if (event.location.isNotBlank()) builder.addField(
|
||||
getEmbedMessage("announcement", "full.field.location", settings),
|
||||
getEmbedMessage("announcement", "full.field.location", settings.locale),
|
||||
event.location.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
)
|
||||
|
||||
if (!announcement.info.isNullOrBlank()) builder.addField(
|
||||
getEmbedMessage("announcement", "full.field.info", settings),
|
||||
getEmbedMessage("announcement", "full.field.info", settings.locale),
|
||||
announcement.info.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
)
|
||||
|
||||
builder.addField(
|
||||
getEmbedMessage("announcement", "full.field.calendar", settings),
|
||||
"${event.calendar.calendarNumber}",
|
||||
getEmbedMessage("announcement", "full.field.calendar", settings.locale),
|
||||
"${event.calendarNumber}",
|
||||
true
|
||||
)
|
||||
builder.addField(getEmbedMessage("announcement", "full.field.event", settings), event.eventId, true)
|
||||
builder.addField(getEmbedMessage("announcement", "full.field.event", settings.locale), event.id, true)
|
||||
|
||||
if (event.image.isNotBlank())
|
||||
builder.image(event.image)
|
||||
|
||||
builder.footer(getEmbedMessage("announcement", "full.footer", settings, announcement.id), null)
|
||||
builder.footer(getEmbedMessage("announcement", "full.footer", settings.locale, announcement.id), null)
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
@@ -395,33 +601,33 @@ class EmbedService(
|
||||
suspend fun simpleAnnouncementEmbed(announcement: Announcement, event: Event, settings: GuildSettings): EmbedCreateSpec {
|
||||
val builder = defaultEmbedBuilder(settings)
|
||||
.color(event.color.asColor())
|
||||
.title(getEmbedMessage("announcement", "simple.title", settings))
|
||||
.title(getEmbedMessage("announcement", "simple.title", settings.locale))
|
||||
|
||||
if (event.name.isNotBlank()) builder.addField(
|
||||
getEmbedMessage("announcement", "simple.field.name", settings),
|
||||
getEmbedMessage("announcement", "simple.field.name", settings.locale),
|
||||
event.name.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
)
|
||||
if (event.description.isNotBlank()) builder.addField(
|
||||
getEmbedMessage("announcement", "simple.field.desc", settings),
|
||||
getEmbedMessage("announcement", "simple.field.desc", settings.locale),
|
||||
event.description.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
)
|
||||
|
||||
builder.addField(
|
||||
getEmbedMessage("announcement", "simple.field.start", settings),
|
||||
event.start.asDiscordTimestamp(DiscordTimestampFormat.LONG_DATETIME),
|
||||
getEmbedMessage("announcement", "simple.field.start", settings.locale),
|
||||
event.start.asDiscordTimestamp(LONG_DATETIME),
|
||||
true
|
||||
)
|
||||
|
||||
if (event.location.isNotBlank()) builder.addField(
|
||||
getEmbedMessage("announcement", "simple.field.location", settings),
|
||||
getEmbedMessage("announcement", "simple.field.location", settings.locale),
|
||||
event.location.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
)
|
||||
|
||||
if (!announcement.info.isNullOrBlank()) builder.addField(
|
||||
getEmbedMessage("announcement", "simple.field.info", settings),
|
||||
getEmbedMessage("announcement", "simple.field.info", settings.locale),
|
||||
announcement.info.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
)
|
||||
@@ -429,7 +635,7 @@ class EmbedService(
|
||||
if (event.image.isNotEmpty())
|
||||
builder.image(event.image)
|
||||
|
||||
builder.footer(getEmbedMessage("announcement", "simple.footer", settings, announcement.id), null)
|
||||
builder.footer(getEmbedMessage("announcement", "simple.footer", settings.locale, announcement.id), null)
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
@@ -437,45 +643,45 @@ class EmbedService(
|
||||
suspend fun eventAnnouncementEmbed(announcement: Announcement, event: Event, settings: GuildSettings): EmbedCreateSpec {
|
||||
val builder = defaultEmbedBuilder(settings)
|
||||
.color(event.color.asColor())
|
||||
.title(getEmbedMessage("announcement", "event.title", settings))
|
||||
.title(getEmbedMessage("announcement", "event.title", settings.locale))
|
||||
|
||||
if (event.name.isNotBlank()) builder.addField(
|
||||
getEmbedMessage("announcement", "event.field.name", settings),
|
||||
getEmbedMessage("announcement", "event.field.name", settings.locale),
|
||||
event.name.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
)
|
||||
if (event.description.isNotBlank()) builder.addField(
|
||||
getEmbedMessage("announcement", "event.field.desc", settings),
|
||||
getEmbedMessage("announcement", "event.field.desc", settings.locale),
|
||||
event.description.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
)
|
||||
|
||||
builder.addField(
|
||||
getEmbedMessage("announcement", "event.field.start", settings),
|
||||
event.start.asDiscordTimestamp(DiscordTimestampFormat.LONG_DATETIME),
|
||||
getEmbedMessage("announcement", "event.field.start", settings.locale),
|
||||
event.start.asDiscordTimestamp(LONG_DATETIME),
|
||||
true
|
||||
)
|
||||
builder.addField(
|
||||
getEmbedMessage("announcement", "event.field.end", settings),
|
||||
event.end.asDiscordTimestamp(DiscordTimestampFormat.LONG_DATETIME),
|
||||
getEmbedMessage("announcement", "event.field.end", settings.locale),
|
||||
event.end.asDiscordTimestamp(LONG_DATETIME),
|
||||
true
|
||||
)
|
||||
|
||||
if (event.location.isNotBlank()) builder.addField(
|
||||
getEmbedMessage("announcement", "event.field.location", settings),
|
||||
getEmbedMessage("announcement", "event.field.location", settings.locale),
|
||||
event.location.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
)
|
||||
|
||||
builder.addField(
|
||||
getEmbedMessage("announcement", "event.field.calendar", settings),
|
||||
"${event.calendar.calendarNumber}",
|
||||
getEmbedMessage("announcement", "event.field.calendar", settings.locale),
|
||||
"${event.calendarNumber}",
|
||||
true
|
||||
)
|
||||
builder.addField(getEmbedMessage("announcement", "event.field.event", settings), event.eventId, true)
|
||||
builder.addField(getEmbedMessage("announcement", "event.field.event", settings.locale), event.id, true)
|
||||
|
||||
if (!announcement.info.isNullOrBlank()) builder.addField(
|
||||
getEmbedMessage("announcement", "event.field.info", settings),
|
||||
getEmbedMessage("announcement", "event.field.info", settings.locale),
|
||||
announcement.info.toMarkdown().embedFieldSafe(),
|
||||
false
|
||||
)
|
||||
@@ -483,47 +689,47 @@ class EmbedService(
|
||||
if (event.image.isNotBlank())
|
||||
builder.image(event.image)
|
||||
|
||||
builder.footer(getEmbedMessage("announcement", "event.footer", settings, announcement.id), null)
|
||||
builder.footer(getEmbedMessage("announcement", "event.footer", settings.locale, announcement.id), null)
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
suspend fun viewAnnouncementEmbed(announcement: Announcement, settings: GuildSettings): EmbedCreateSpec {
|
||||
val builder = defaultEmbedBuilder(settings)
|
||||
.title(getEmbedMessage("announcement", "view.title", settings))
|
||||
.addField(getEmbedMessage("announcement", "view.field.type", settings), announcement.type.name, true)
|
||||
.addField(getEmbedMessage("announcement", "view.field.modifier", settings), announcement.modifier.name, true)
|
||||
.addField(getEmbedMessage("announcement", "view.field.channel", settings), "<#${announcement.channelId.asLong()}>", false)
|
||||
.addField(getEmbedMessage("announcement", "view.field.hours", settings), "${announcement.hoursBefore}", true)
|
||||
.addField(getEmbedMessage("announcement", "view.field.minutes", settings), "${announcement.minutesBefore}", true)
|
||||
.title(getEmbedMessage("announcement", "view.title", settings.locale))
|
||||
.addField(getEmbedMessage("announcement", "view.field.type", settings.locale), announcement.type.name, true)
|
||||
.addField(getEmbedMessage("announcement", "view.field.modifier", settings.locale), announcement.modifier.name, true)
|
||||
.addField(getEmbedMessage("announcement", "view.field.channel", settings.locale), "<#${announcement.channelId.asLong()}>", false)
|
||||
.addField(getEmbedMessage("announcement", "view.field.hours", settings.locale), "${announcement.hoursBefore}", true)
|
||||
.addField(getEmbedMessage("announcement", "view.field.minutes", settings.locale), "${announcement.minutesBefore}", true)
|
||||
|
||||
if (!announcement.info.isNullOrBlank()) {
|
||||
builder.addField(getEmbedMessage("announcement", "view.field.info", settings), announcement.info.toMarkdown().embedFieldSafe(), false)
|
||||
builder.addField(getEmbedMessage("announcement", "view.field.info", settings.locale), announcement.info.toMarkdown().embedFieldSafe(), false)
|
||||
}
|
||||
|
||||
builder.addField(getEmbedMessage("announcement", "view.field.calendar", settings), "${announcement.calendarNumber}", true)
|
||||
builder.addField(getEmbedMessage("announcement", "view.field.calendar", settings.locale), "${announcement.calendarNumber}", true)
|
||||
|
||||
if (announcement.type == Announcement.Type.RECUR || announcement.type == Announcement.Type.SPECIFIC)
|
||||
builder.addField(getEmbedMessage("announcement", "view.field.event", settings), announcement.eventId!!, true)
|
||||
builder.addField(getEmbedMessage("announcement", "view.field.event", settings.locale), announcement.eventId!!, true)
|
||||
|
||||
if (announcement.type == Announcement.Type.COLOR) {
|
||||
builder.color(announcement.eventColor.asColor())
|
||||
builder.addField(getEmbedMessage("announcement", "view.field.color", settings), announcement.eventColor.name, true)
|
||||
builder.addField(getEmbedMessage("announcement", "view.field.color", settings.locale), announcement.eventColor.name, true)
|
||||
} else builder.color(GlobalVal.discalColor)
|
||||
|
||||
return builder.addField(getEmbedMessage("announcement", "view.field.id", settings), announcement.id, false)
|
||||
.addField(getEmbedMessage("announcement", "view.field.enabled", settings), "${announcement.enabled}", true)
|
||||
.addField(getEmbedMessage("announcement", "view.field.publish", settings), "${announcement.publish}", true)
|
||||
return builder.addField(getEmbedMessage("announcement", "view.field.id", settings.locale), announcement.id, false)
|
||||
.addField(getEmbedMessage("announcement", "view.field.enabled", settings.locale), "${announcement.enabled}", true)
|
||||
.addField(getEmbedMessage("announcement", "view.field.publish", settings.locale), "${announcement.publish}", true)
|
||||
.build()
|
||||
}
|
||||
|
||||
suspend fun condensedAnnouncementEmbed(announcement: Announcement, settings: GuildSettings): EmbedCreateSpec {
|
||||
val builder = defaultEmbedBuilder(settings)
|
||||
.title(getEmbedMessage("announcement", "con.title", settings))
|
||||
.addField(getEmbedMessage("announcement", "con.field.id", settings), announcement.id, false)
|
||||
.addField(getEmbedMessage("announcement", "con.field.time", settings), "${announcement.hoursBefore}H${announcement.minutesBefore}m", true)
|
||||
.addField(getEmbedMessage("announcement", "con.field.enabled", settings), "${announcement.enabled}", true)
|
||||
.footer(getEmbedMessage("announcement", "con.footer", settings, announcement.type.name, announcement.modifier.name), null)
|
||||
.title(getEmbedMessage("announcement", "con.title", settings.locale))
|
||||
.addField(getEmbedMessage("announcement", "con.field.id", settings.locale), announcement.id, false)
|
||||
.addField(getEmbedMessage("announcement", "con.field.time", settings.locale), "${announcement.hoursBefore}H${announcement.minutesBefore}m", true)
|
||||
.addField(getEmbedMessage("announcement", "con.field.enabled", settings.locale), "${announcement.enabled}", true)
|
||||
.footer(getEmbedMessage("announcement", "con.footer", settings.locale, announcement.type.name, announcement.modifier.name), null)
|
||||
|
||||
if (announcement.type == Announcement.Type.COLOR) builder.color(announcement.eventColor.asColor())
|
||||
else builder.color(GlobalVal.discalColor)
|
||||
@@ -531,24 +737,24 @@ class EmbedService(
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
suspend fun announcementWizardEmbed(wizard: WizardState<Announcement>, settings: GuildSettings): EmbedCreateSpec {
|
||||
suspend fun announcementWizardEmbed(wizard: AnnouncementWizardState, settings: GuildSettings): EmbedCreateSpec {
|
||||
val announcement = wizard.entity
|
||||
|
||||
val builder = defaultEmbedBuilder(settings)
|
||||
.title(getEmbedMessage("announcement", "wizard.title", settings))
|
||||
.footer(getEmbedMessage("announcement", "wizard.footer", settings), null)
|
||||
.title(getEmbedMessage("announcement", "wizard.title", settings.locale))
|
||||
.footer(getEmbedMessage("announcement", "wizard.footer", settings.locale), null)
|
||||
.color(announcement.eventColor.asColor())
|
||||
//fields
|
||||
.addField(getEmbedMessage("announcement", "wizard.field.type", settings), announcement.type.name, true)
|
||||
.addField(getEmbedMessage("announcement", "wizard.field.modifier", settings), announcement.modifier.name, true)
|
||||
.addField(getEmbedMessage("announcement", "wizard.field.type", settings.locale), announcement.type.name, true)
|
||||
.addField(getEmbedMessage("announcement", "wizard.field.modifier", settings.locale), announcement.modifier.name, true)
|
||||
|
||||
if (announcement.type == Announcement.Type.COLOR) {
|
||||
if (announcement.eventColor == EventColor.NONE) builder.addField(
|
||||
getEmbedMessage("announcement", "wizard.field.color", settings),
|
||||
getCommonMsg("embed.unset", settings),
|
||||
getEmbedMessage("announcement", "wizard.field.color", settings.locale),
|
||||
getCommonMsg("embed.unset", settings.locale),
|
||||
false
|
||||
) else builder.addField(
|
||||
getEmbedMessage("announcement", "wizard.field.color", settings),
|
||||
getEmbedMessage("announcement", "wizard.field.color", settings.locale),
|
||||
announcement.eventColor.name,
|
||||
false
|
||||
)
|
||||
@@ -556,56 +762,56 @@ class EmbedService(
|
||||
|
||||
if (announcement.type == Announcement.Type.SPECIFIC || announcement.type == Announcement.Type.RECUR) {
|
||||
if (announcement.eventId.isNullOrBlank()) builder.addField(
|
||||
getEmbedMessage("announcement", "wizard.field.event", settings),
|
||||
getCommonMsg("embed.unset", settings),
|
||||
getEmbedMessage("announcement", "wizard.field.event", settings.locale),
|
||||
getCommonMsg("embed.unset", settings.locale),
|
||||
false
|
||||
) else builder.addField(
|
||||
getEmbedMessage("announcement", "wizard.field.event", settings),
|
||||
getEmbedMessage("announcement", "wizard.field.event", settings.locale),
|
||||
announcement.eventId,
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
if (announcement.info.isNullOrBlank()) builder.addField(
|
||||
getEmbedMessage("announcement", "wizard.field.info", settings),
|
||||
getCommonMsg("embed.unset", settings),
|
||||
getEmbedMessage("announcement", "wizard.field.info", settings.locale),
|
||||
getCommonMsg("embed.unset", settings.locale),
|
||||
false
|
||||
) else builder.addField(
|
||||
getEmbedMessage("announcement", "wizard.field.info", settings),
|
||||
getEmbedMessage("announcement", "wizard.field.info", settings.locale),
|
||||
announcement.info.embedFieldSafe().toMarkdown(),
|
||||
false
|
||||
)
|
||||
|
||||
builder.addField(getEmbedMessage("announcement", "wizard.field.channel", settings), "<#${announcement.channelId.asLong()}>", false)
|
||||
builder.addField(getEmbedMessage("announcement", "wizard.field.minutes", settings), "${announcement.minutesBefore}", true)
|
||||
builder.addField(getEmbedMessage("announcement", "wizard.field.hours", settings), "${announcement.hoursBefore}", true)
|
||||
builder.addField(getEmbedMessage("announcement", "wizard.field.channel", settings.locale), "<#${announcement.channelId.asLong()}>", false)
|
||||
builder.addField(getEmbedMessage("announcement", "wizard.field.minutes", settings.locale), "${announcement.minutesBefore}", true)
|
||||
builder.addField(getEmbedMessage("announcement", "wizard.field.hours", settings.locale), "${announcement.hoursBefore}", true)
|
||||
|
||||
if (wizard.editing) builder.addField(getEmbedMessage("announcement", "wizard.field.id", settings), announcement.id, false)
|
||||
if (wizard.editing) builder.addField(getEmbedMessage("announcement", "wizard.field.id", settings.locale), announcement.id, false)
|
||||
else builder.addField(
|
||||
getEmbedMessage("announcement", "wizard.field.id", settings),
|
||||
getCommonMsg("embed.unset", settings),
|
||||
getEmbedMessage("announcement", "wizard.field.id", settings.locale),
|
||||
getCommonMsg("embed.unset", settings.locale),
|
||||
false
|
||||
)
|
||||
|
||||
builder.addField(getEmbedMessage("announcement", "wizard.field.publish", settings), "${announcement.publish}", true)
|
||||
builder.addField(getEmbedMessage("announcement", "wizard.field.enabled", settings), "${announcement.enabled}", true)
|
||||
builder.addField(getEmbedMessage("announcement", "wizard.field.calendar", settings), "${announcement.calendarNumber}", true)
|
||||
builder.addField(getEmbedMessage("announcement", "wizard.field.publish", settings.locale), "${announcement.publish}", true)
|
||||
builder.addField(getEmbedMessage("announcement", "wizard.field.enabled", settings.locale), "${announcement.enabled}", true)
|
||||
builder.addField(getEmbedMessage("announcement", "wizard.field.calendar", settings.locale), "${announcement.calendarNumber}", true)
|
||||
|
||||
// Build up any warnings
|
||||
val warningsBuilder = StringBuilder()
|
||||
if ((announcement.type == Announcement.Type.SPECIFIC || announcement.type == Announcement.Type.RECUR) && announcement.eventId.isNullOrBlank())
|
||||
warningsBuilder.appendLine(getEmbedMessage("announcement", "warning.wizard.eventId", settings)).appendLine()
|
||||
warningsBuilder.appendLine(getEmbedMessage("announcement", "warning.wizard.eventId", settings.locale)).appendLine()
|
||||
if (announcement.type == Announcement.Type.COLOR && announcement.eventColor == EventColor.NONE)
|
||||
warningsBuilder.appendLine(getEmbedMessage("announcement", "warning.wizard.color", settings)).appendLine()
|
||||
warningsBuilder.appendLine(getEmbedMessage("announcement", "warning.wizard.color", settings.locale)).appendLine()
|
||||
if (announcement.getCalculatedTime() < Duration.ofMinutes(5))
|
||||
warningsBuilder.appendLine(getEmbedMessage("announcement", "warning.wizard.time", settings)).appendLine()
|
||||
warningsBuilder.appendLine(getEmbedMessage("announcement", "warning.wizard.time", settings.locale)).appendLine()
|
||||
if (announcement.calendarNumber > settings.maxCalendars)
|
||||
warningsBuilder.appendLine(getEmbedMessage("announcement", "warning.wizard.calNum", settings))
|
||||
warningsBuilder.appendLine(getEmbedMessage("announcement", "warning.wizard.calNum", settings.locale))
|
||||
|
||||
|
||||
|
||||
if (warningsBuilder.isNotBlank()) {
|
||||
builder.addField(getEmbedMessage("announcement", "wizard.field.warnings", settings), warningsBuilder.toString(), false)
|
||||
builder.addField(getEmbedMessage("announcement", "wizard.field.warnings", settings.locale), warningsBuilder.toString(), false)
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
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.EventCache
|
||||
import org.dreamexposure.discal.core.database.EventMetadataData
|
||||
import org.dreamexposure.discal.core.database.EventMetadataRepository
|
||||
import org.dreamexposure.discal.core.`object`.new.EventMetadata
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class EventMetadataService(
|
||||
private val eventMetadataRepository: EventMetadataRepository,
|
||||
private val eventCache: EventCache,
|
||||
) {
|
||||
|
||||
/////////
|
||||
/// Event metadata - Prefer using full Event implementation in CalendarService
|
||||
/////////
|
||||
suspend fun hasEventMetadata(guildId: Snowflake, eventId: String): Boolean {
|
||||
val computedId = eventId.split("_")[0]
|
||||
|
||||
return eventMetadataRepository.existsByGuildIdAndEventId(guildId.asLong(), computedId)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
suspend fun getEventMetadata(guildId: Snowflake, eventId: String): EventMetadata? {
|
||||
val computedId = eventId.split("_")[0]
|
||||
|
||||
return eventMetadataRepository.findByGuildIdAndEventId(guildId.asLong(), computedId)
|
||||
.map(::EventMetadata)
|
||||
.awaitSingleOrNull()
|
||||
}
|
||||
|
||||
suspend fun getMultipleEventsMetadata(guildId: Snowflake, eventIds: List<String>): List<EventMetadata> {
|
||||
val computedIds = eventIds.map { eventId -> eventId.split("_")[0] }
|
||||
|
||||
return eventMetadataRepository.findAllByGuildIdAndEventIdIn(guildId.asLong(), computedIds)
|
||||
.map(::EventMetadata)
|
||||
.collectList()
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
suspend fun createEventMetadata(event: EventMetadata): EventMetadata {
|
||||
val computedId = event.id.split("_")[0]
|
||||
|
||||
return eventMetadataRepository.save(EventMetadataData(
|
||||
guildId = event.guildId.asLong(),
|
||||
eventId = computedId,
|
||||
calendarNumber = event.calendarNumber,
|
||||
eventEnd = event.eventEnd.toEpochMilli(),
|
||||
imageLink = event.imageLink,
|
||||
)).map(::EventMetadata).awaitSingle()
|
||||
}
|
||||
|
||||
suspend fun updateEventMetadata(event: EventMetadata) {
|
||||
val computedId = event.id.split("_")[0]
|
||||
|
||||
eventMetadataRepository.updateByGuildIdAndEventId(
|
||||
guildId = event.guildId.asLong(),
|
||||
eventId = computedId,
|
||||
calendarNumber = event.calendarNumber,
|
||||
eventEnd = event.eventEnd.toEpochMilli(),
|
||||
imageLink = event.imageLink
|
||||
).awaitSingleOrNull()
|
||||
}
|
||||
|
||||
suspend fun upsertEventMetadata(event: EventMetadata) {
|
||||
if (hasEventMetadata(event.guildId, event.id)) updateEventMetadata(event)
|
||||
else createEventMetadata(event)
|
||||
}
|
||||
|
||||
suspend fun deleteEventMetadata(guildId: Snowflake, id: String) {
|
||||
if (id.contains("_")) return // Don't delete if child of recurring parent.
|
||||
|
||||
eventMetadataRepository.deleteByGuildIdAndEventId(guildId.asLong(), id).awaitSingleOrNull()
|
||||
}
|
||||
|
||||
suspend fun deleteEventMetadataForCalendarDeletion(guildId: Snowflake, calendarNumber: Int) {
|
||||
eventMetadataRepository.deleteAllByGuildIdAndCalendarNumber(guildId.asLong(), calendarNumber).awaitSingleOrNull()
|
||||
eventMetadataRepository.decrementCalendarsByGuildIdAndCalendarNumber(guildId.asLong(), calendarNumber).awaitSingleOrNull()
|
||||
eventCache.evictAll(guildId)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
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.GuildSettingsCache
|
||||
import org.dreamexposure.discal.core.database.GuildSettingsData
|
||||
import org.dreamexposure.discal.core.database.GuildSettingsRepository
|
||||
import org.dreamexposure.discal.core.logger.LOGGER
|
||||
import org.dreamexposure.discal.core.`object`.new.GuildSettings
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class GuildSettingsService(
|
||||
private val repository: GuildSettingsRepository,
|
||||
private val cache: GuildSettingsCache,
|
||||
) {
|
||||
|
||||
suspend fun hasSettings(guildId: Snowflake): Boolean {
|
||||
return repository.existsByGuildId(guildId.asLong()).awaitSingle()
|
||||
}
|
||||
|
||||
suspend fun getSettings(guildId: Snowflake): GuildSettings {
|
||||
var settings = cache.get(key = guildId)
|
||||
if (settings != null) return settings
|
||||
|
||||
settings = repository.findByGuildId(guildId.asLong())
|
||||
.map(::GuildSettings)
|
||||
.defaultIfEmpty(GuildSettings(guildId = guildId))
|
||||
.awaitSingle()
|
||||
|
||||
cache.put(key = guildId, value = settings)
|
||||
return settings
|
||||
}
|
||||
|
||||
suspend fun createSettings(settings: GuildSettings): GuildSettings {
|
||||
LOGGER.debug("Creating new settings for guild: {}", settings.guildId)
|
||||
|
||||
val saved = repository.save(GuildSettingsData(
|
||||
guildId = settings.guildId.asLong(),
|
||||
|
||||
controlRole = settings.controlRole?.asString() ?: "everyone",
|
||||
timeFormat = settings.interfaceStyle.timeFormat.value,
|
||||
patronGuild = settings.patronGuild,
|
||||
devGuild = settings.devGuild,
|
||||
maxCalendars = settings.maxCalendars,
|
||||
lang = settings.locale.toLanguageTag(),
|
||||
branded = settings.interfaceStyle.branded,
|
||||
announcementStyle = settings.interfaceStyle.announcementStyle.value,
|
||||
eventKeepDuration = settings.eventKeepDuration,
|
||||
)).map(::GuildSettings).awaitSingle()
|
||||
|
||||
cache.put(key = saved.guildId, value = saved)
|
||||
return saved
|
||||
}
|
||||
|
||||
suspend fun updateSettings(settings: GuildSettings) {
|
||||
LOGGER.debug("Updating guild settings for {}", settings.guildId)
|
||||
|
||||
repository.updateByGuildId(
|
||||
guildId = settings.guildId.asLong(),
|
||||
|
||||
controlRole = settings.controlRole?.asString() ?: "everyone",
|
||||
timeFormat = settings.interfaceStyle.timeFormat.value,
|
||||
patronGuild = settings.patronGuild,
|
||||
devGuild = settings.devGuild,
|
||||
maxCalendars = settings.maxCalendars,
|
||||
lang = settings.locale.toLanguageTag(),
|
||||
branded = settings.interfaceStyle.branded,
|
||||
announcementStyle = settings.interfaceStyle.announcementStyle.value,
|
||||
eventKeepDuration = settings.eventKeepDuration,
|
||||
).awaitSingleOrNull()
|
||||
|
||||
cache.put(key = settings.guildId, value = settings)
|
||||
}
|
||||
|
||||
suspend fun upsertSettings(settings: GuildSettings): GuildSettings {
|
||||
if (hasSettings(settings.guildId)) updateSettings(settings)
|
||||
else return createSettings(settings)
|
||||
return settings
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.dreamexposure.discal.core.business
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.dreamexposure.discal.core.logger.LOGGER
|
||||
import org.springframework.stereotype.Component
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.net.MalformedURLException
|
||||
import java.net.URI
|
||||
import java.time.Duration
|
||||
import javax.imageio.ImageIO
|
||||
import javax.imageio.ImageReader
|
||||
|
||||
@Component
|
||||
class ImageValidationService {
|
||||
suspend fun validate(url: String, allowGif: Boolean): Boolean = withContext(Dispatchers.IO) {
|
||||
return@withContext try {
|
||||
val image = ImageIO.read(URI.create(url).toURL())
|
||||
image != null
|
||||
} catch (_: IOException) {
|
||||
if (allowGif) validateGif(url)
|
||||
else false
|
||||
} catch (_: MalformedURLException) {
|
||||
false
|
||||
} catch (_: FileNotFoundException) {
|
||||
false
|
||||
} catch (ex: Exception) {
|
||||
LOGGER.error("Image validation failed with unexpected exception", ex)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun validateGif(url: String): Boolean = withContext(Dispatchers.IO) {
|
||||
val connection = URI.create(url).toURL().openConnection()
|
||||
connection.connectTimeout = Duration.ofSeconds(3).toMillis().toInt()
|
||||
connection.readTimeout = Duration.ofSeconds(3).toMillis().toInt()
|
||||
|
||||
readGif(connection.inputStream)?.equals(".gif", true) == true
|
||||
}
|
||||
|
||||
private suspend fun readGif(input: InputStream): String? = withContext(Dispatchers.IO) {
|
||||
val stream = ImageIO.createImageInputStream(input)
|
||||
val iterator = ImageIO.getImageReaders(stream)
|
||||
if (!iterator.hasNext()) return@withContext null
|
||||
|
||||
var reader: ImageReader? = null
|
||||
try {
|
||||
reader = iterator.next()
|
||||
|
||||
reader.setInput(stream, true, true)
|
||||
reader.read(0, reader.defaultReadParam)
|
||||
} catch (_: Exception) {
|
||||
|
||||
} finally {
|
||||
reader?.dispose()
|
||||
}
|
||||
|
||||
reader?.formatName
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.dreamexposure.discal.core.business
|
||||
|
||||
import discord4j.common.util.Snowflake
|
||||
import discord4j.core.DiscordClient
|
||||
import discord4j.discordjson.json.RoleData
|
||||
import discord4j.rest.util.Permission
|
||||
import discord4j.rest.util.PermissionSet
|
||||
import kotlinx.coroutines.reactor.awaitSingle
|
||||
import org.springframework.beans.factory.BeanFactory
|
||||
import org.springframework.beans.factory.getBean
|
||||
import org.springframework.stereotype.Component
|
||||
import java.util.function.Predicate
|
||||
|
||||
@Component
|
||||
class PermissionService(
|
||||
private val settingsService: GuildSettingsService,
|
||||
private val beanFactory: BeanFactory
|
||||
) {
|
||||
private val discordClient
|
||||
get() = beanFactory.getBean<DiscordClient>()
|
||||
|
||||
suspend fun hasControlRole(guildId: Snowflake, memberId: Snowflake): Boolean {
|
||||
val settings = settingsService.getSettings(guildId)
|
||||
|
||||
if (settings.controlRole == null || settings.controlRole == guildId) return true
|
||||
|
||||
val memberData = discordClient.getMemberById(guildId, memberId).data.awaitSingle()
|
||||
|
||||
return memberData.roles().map(Snowflake::of).contains(settings.controlRole)
|
||||
}
|
||||
|
||||
suspend fun hasPermissions(guildId: Snowflake, memberId: Snowflake, pred: Predicate<PermissionSet>): Boolean {
|
||||
val guildData = discordClient.getGuildById(guildId).data.awaitSingle()
|
||||
|
||||
// Owner has full permissions, always
|
||||
if (guildData.ownerId().asLong() == memberId.asLong()) return true
|
||||
|
||||
val memberData = discordClient.getMemberById(guildId, memberId).data.awaitSingle()
|
||||
|
||||
val computedPermissions = PermissionSet.of(
|
||||
guildData.roles()
|
||||
.filter { memberData.roles().contains(it.id()) }
|
||||
.map(RoleData::permissions)
|
||||
.reduceOrNull { acc, lng -> acc or lng } ?: 0L
|
||||
)
|
||||
|
||||
return pred.test(computedPermissions)
|
||||
}
|
||||
|
||||
suspend fun hasElevatedPermissions(guildId: Snowflake, memberId: Snowflake): Boolean {
|
||||
return hasPermissions(guildId, memberId) {
|
||||
it.contains(Permission.MANAGE_GUILD) || it.contains(Permission.ADMINISTRATOR)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ class RsvpService(
|
||||
private val rsvpRepository: RsvpRepository,
|
||||
private val rsvpCache: RsvpCache,
|
||||
private val embedService: EmbedService,
|
||||
private val calendarService: CalendarService,
|
||||
private val beanFactory: BeanFactory,
|
||||
) {
|
||||
private val discordClient: DiscordClient
|
||||
@@ -78,11 +79,11 @@ class RsvpService(
|
||||
|
||||
// Validate that role exists if changed
|
||||
if (new.role != null && old.role != new.role) {
|
||||
val exists = discordClient.getRoleById(new.guildId, new.role!!).data
|
||||
val exists = discordClient.getRoleById(new.guildId, new.role).data
|
||||
.transform(ClientException.emptyOnStatus(GlobalVal.STATUS_NOT_FOUND))
|
||||
.hasElement()
|
||||
.awaitSingle()
|
||||
if (!exists) throw NotFoundException("Role not found for guild:${new.guildId.asString()} role:${new.role!!.asString()}")
|
||||
if (!exists) throw NotFoundException("Role not found for guild:${new.guildId.asString()} role:${new.role.asString()}")
|
||||
}
|
||||
|
||||
// Handle role change (remove roles, store to-add in list for later)
|
||||
@@ -170,8 +171,10 @@ class RsvpService(
|
||||
}
|
||||
|
||||
// Send out DMs
|
||||
val event = calendarService.getEvent(rsvp.guildId, rsvp.calendarNumber, rsvp.eventId) ?: throw RuntimeException("This really should just not be possible lmao")
|
||||
|
||||
toDm.forEach { userId ->
|
||||
val embed = embedService.rsvpDmFollowupEmbed(new, userId)
|
||||
val embed = embedService.rsvpDmFollowupEmbed(new, event, userId)
|
||||
|
||||
discordClient.getUserById(userId).privateChannel.flatMap { channelData ->
|
||||
discordClient.getChannelById(Snowflake.of(channelData.id()))
|
||||
@@ -204,4 +207,10 @@ class RsvpService(
|
||||
.forEach { rsvpCache.put(guildId, it.eventId, it) }
|
||||
}
|
||||
|
||||
suspend fun deleteRsvpForCalendarDeletion(guildId: Snowflake, calendarNumber: Int) {
|
||||
rsvpRepository.deleteAllByGuildIdAndCalendarNumber(guildId.asLong(), calendarNumber).awaitSingleOrNull()
|
||||
rsvpRepository.decrementCalendarsByGuildIdAndCalendarNumber(guildId.asLong(), calendarNumber).awaitSingleOrNull()
|
||||
rsvpCache.evictAll(guildId)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,11 +8,10 @@ import discord4j.rest.http.client.ClientException
|
||||
import kotlinx.coroutines.reactor.awaitSingle
|
||||
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
||||
import org.dreamexposure.discal.StaticMessageCache
|
||||
import org.dreamexposure.discal.core.config.Config
|
||||
import org.dreamexposure.discal.core.database.StaticMessageData
|
||||
import org.dreamexposure.discal.core.database.StaticMessageRepository
|
||||
import org.dreamexposure.discal.core.exceptions.NotFoundException
|
||||
import org.dreamexposure.discal.core.extensions.discord4j.getCalendar
|
||||
import org.dreamexposure.discal.core.extensions.discord4j.getSettings
|
||||
import org.dreamexposure.discal.core.`object`.new.StaticMessage
|
||||
import org.springframework.beans.factory.BeanFactory
|
||||
import org.springframework.beans.factory.getBean
|
||||
@@ -27,6 +26,7 @@ import java.time.temporal.ChronoUnit
|
||||
class StaticMessageService(
|
||||
private val staticMessageRepository: StaticMessageRepository,
|
||||
private val staticMessageCache: StaticMessageCache,
|
||||
private val calendarService: CalendarService,
|
||||
private val embedService: EmbedService,
|
||||
private val componentService: ComponentService,
|
||||
private val metricService: MetricService,
|
||||
@@ -34,6 +34,7 @@ class StaticMessageService(
|
||||
) {
|
||||
private val discordClient: DiscordClient
|
||||
get() = beanFactory.getBean()
|
||||
private val OVERVIEW_EVENT_COUNT = Config.CALENDAR_OVERVIEW_DEFAULT_EVENT_COUNT.getInt()
|
||||
|
||||
suspend fun getStaticMessageCount() = staticMessageRepository.count().awaitSingle()
|
||||
|
||||
@@ -75,12 +76,10 @@ class StaticMessageService(
|
||||
): StaticMessage {
|
||||
|
||||
// Gather everything we need
|
||||
val settings = discordClient.getGuildById(guildId).getSettings().awaitSingle()
|
||||
val calendar = discordClient.getGuildById(guildId)
|
||||
.getCalendar(calendarNumber)
|
||||
.awaitSingleOrNull() ?: throw NotFoundException("Calendar not found")
|
||||
val calendar = calendarService.getCalendar(guildId, calendarNumber) ?: throw NotFoundException("Calendar not found")
|
||||
val events = calendarService.getUpcomingEvents(guildId, calendarNumber, OVERVIEW_EVENT_COUNT)
|
||||
val channel = discordClient.getChannelById(channelId)
|
||||
val embed = embedService.calendarOverviewEmbed(calendar, settings, showUpdate = true)
|
||||
val embed = embedService.calendarOverviewEmbed(calendar, events, showUpdate = true)
|
||||
val nextUpdate = ZonedDateTime.now(calendar.timezone)
|
||||
.truncatedTo(ChronoUnit.DAYS)
|
||||
.plusHours(updateHour + 24)
|
||||
@@ -127,13 +126,11 @@ class StaticMessageService(
|
||||
return
|
||||
}
|
||||
|
||||
val settings = discordClient.getGuildById(guildId).getSettings().awaitSingle()
|
||||
val calendar = discordClient.getGuildById(guildId)
|
||||
.getCalendar(old.calendarNumber)
|
||||
.awaitSingleOrNull() ?: throw NotFoundException("Calendar not found")
|
||||
val calendar = calendarService.getCalendar(guildId, old.calendarNumber) ?: throw NotFoundException("Calendar not found")
|
||||
val events = calendarService.getUpcomingEvents(guildId, old.calendarNumber, OVERVIEW_EVENT_COUNT)
|
||||
|
||||
// Finally update the message
|
||||
val embed = embedService.calendarOverviewEmbed(calendar, settings, showUpdate = true)
|
||||
val embed = embedService.calendarOverviewEmbed(calendar, events, showUpdate = true)
|
||||
|
||||
discordClient.getMessageById(old.channelId, old.messageId).edit(
|
||||
MessageEditRequest.builder()
|
||||
@@ -168,11 +165,9 @@ class StaticMessageService(
|
||||
taskTimer.start()
|
||||
|
||||
val oldVersions = getStaticMessagesForCalendar(guildId, calendarNumber)
|
||||
val settings = discordClient.getGuildById(guildId).getSettings().awaitSingle()
|
||||
val calendar = discordClient.getGuildById(guildId)
|
||||
.getCalendar(calendarNumber)
|
||||
.awaitSingleOrNull() ?: throw NotFoundException("Calendar not found")
|
||||
val embed = embedService.calendarOverviewEmbed(calendar, settings, showUpdate = true)
|
||||
val calendar = calendarService.getCalendar(guildId, calendarNumber) ?: throw NotFoundException("Calendar not found")
|
||||
val events = calendarService.getUpcomingEvents(guildId, calendarNumber, OVERVIEW_EVENT_COUNT)
|
||||
val embed = embedService.calendarOverviewEmbed(calendar, events, showUpdate = true)
|
||||
|
||||
oldVersions.forEach { old ->
|
||||
val existingData = discordClient.getMessageById(old.channelId, old.messageId)
|
||||
@@ -215,7 +210,13 @@ class StaticMessageService(
|
||||
}
|
||||
|
||||
suspend fun deleteStaticMessage(guildId: Snowflake, messageId: Snowflake) {
|
||||
staticMessageRepository.deleteByGuildIdAndMessageId(guildId.asLong(), messageId.asLong()).awaitSingleOrNull()
|
||||
staticMessageRepository.deleteAllByGuildIdAndMessageId(guildId.asLong(), messageId.asLong()).awaitSingleOrNull()
|
||||
staticMessageCache.evict(guildId, key = messageId)
|
||||
}
|
||||
|
||||
suspend fun deleteStaticMessagesForCalendarDeletion(guildId: Snowflake, calendarNumber: Int) {
|
||||
staticMessageRepository.deleteByGuildIdAndCalendarNumber(guildId.asLong(), calendarNumber).awaitSingleOrNull()
|
||||
staticMessageRepository.decrementCalendarsByGuildIdAndCalendarNumber(guildId.asLong(), calendarNumber).awaitSingleOrNull()
|
||||
staticMessageCache.evictAll(guildId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.dreamexposure.discal.core.business.api
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.executeAsync
|
||||
import org.dreamexposure.discal.core.logger.LOGGER
|
||||
import org.dreamexposure.discal.core.`object`.new.model.ResponseModel
|
||||
import org.dreamexposure.discal.core.`object`.rest.ErrorResponse
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class ApiWrapperClient(
|
||||
private val httpClient: OkHttpClient,
|
||||
private val objectMapper: ObjectMapper,
|
||||
) {
|
||||
internal suspend fun <T> makeRequest(request: Request, valueType: Class<T>): ResponseModel<T> {
|
||||
var response: Response? = null
|
||||
|
||||
try {
|
||||
response = httpClient.newCall(request).executeAsync()
|
||||
|
||||
when (response.code) {
|
||||
200 -> {
|
||||
val data = objectMapper.readValue(response.body!!.string(), valueType)
|
||||
response.body?.close()
|
||||
response.close()
|
||||
|
||||
return ResponseModel(data)
|
||||
}
|
||||
else -> {
|
||||
val error = objectMapper.readValue<ErrorResponse>(response.body!!.string())
|
||||
response.body?.close()
|
||||
response.close()
|
||||
|
||||
return ResponseModel(error, response.code)
|
||||
}
|
||||
}
|
||||
|
||||
} catch (ex: Exception) {
|
||||
LOGGER.error("Error making request host:${request.url.host} | uri:${request.url.encodedPath} | code:${response?.code}", ex)
|
||||
throw ex // Rethrow and let implementation decide proper handling for exception
|
||||
} finally {
|
||||
response?.body?.close()
|
||||
response?.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package org.dreamexposure.discal.core.business.api
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import discord4j.common.util.Snowflake
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.dreamexposure.discal.core.config.Config
|
||||
import org.dreamexposure.discal.core.enums.calendar.CalendarHost
|
||||
import org.dreamexposure.discal.core.logger.LOGGER
|
||||
import org.dreamexposure.discal.core.`object`.new.model.ResponseModel
|
||||
import org.dreamexposure.discal.core.`object`.new.model.discal.cam.SecurityValidateV1Request
|
||||
import org.dreamexposure.discal.core.`object`.new.model.discal.cam.SecurityValidateV1Response
|
||||
import org.dreamexposure.discal.core.`object`.new.model.discal.cam.TokenV1Model
|
||||
import org.dreamexposure.discal.core.utils.GlobalVal.JSON
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class CamApiWrapper(
|
||||
private val apiWrapperClient: ApiWrapperClient,
|
||||
private val objectMapper: ObjectMapper,
|
||||
) {
|
||||
private final val CAM_URL = Config.URL_CAM.getString()
|
||||
private final val AUTH_HEADER = "Int ${Config.SECRET_DISCAL_API_KEY.getString()}"
|
||||
|
||||
suspend fun validateToken(requestBody: SecurityValidateV1Request): ResponseModel<SecurityValidateV1Response> {
|
||||
val request = Request.Builder()
|
||||
.url("${Config.URL_CAM.getString()}/v1/security/validate")
|
||||
.post(objectMapper.writeValueAsString(requestBody).toRequestBody(JSON))
|
||||
.header("Authorization", AUTH_HEADER)
|
||||
.header("Content-Type", "application/json")
|
||||
.build()
|
||||
|
||||
return apiWrapperClient.makeRequest(request, SecurityValidateV1Response::class.java)
|
||||
}
|
||||
|
||||
suspend fun getCalendarToken(credentialId: Int): ResponseModel<TokenV1Model> {
|
||||
LOGGER.debug("Getting calendar token for credential:$credentialId")
|
||||
|
||||
val url = "$CAM_URL/v1/token".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("host", CalendarHost.GOOGLE.name)
|
||||
.addQueryParameter("id", credentialId.toString())
|
||||
.build()
|
||||
|
||||
val request = Request.Builder().get()
|
||||
.header("Authorization", AUTH_HEADER)
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
return apiWrapperClient.makeRequest(request, TokenV1Model::class.java)
|
||||
}
|
||||
|
||||
suspend fun getCalendarToken(guildId: Snowflake, calNumber: Int, host: CalendarHost): ResponseModel<TokenV1Model> {
|
||||
LOGGER.debug("Getting calendar token for guild:{} | host:{} | calendarId:{} ", guildId.asLong(), host.name, calNumber)
|
||||
|
||||
val url = "$CAM_URL/v1/token".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("host", host.name)
|
||||
.addQueryParameter("guild", guildId.asString())
|
||||
.addQueryParameter("id", calNumber.toString())
|
||||
.build()
|
||||
|
||||
val request = Request.Builder().get()
|
||||
.header("Authorization", AUTH_HEADER)
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
return apiWrapperClient.makeRequest(request, TokenV1Model::class.java)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.dreamexposure.discal.core.business.google
|
||||
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Request
|
||||
import org.dreamexposure.discal.core.business.api.ApiWrapperClient
|
||||
import org.dreamexposure.discal.core.config.Config
|
||||
import org.dreamexposure.discal.core.exceptions.AccessRevokedException
|
||||
import org.dreamexposure.discal.core.logger.LOGGER
|
||||
import org.dreamexposure.discal.core.`object`.new.model.ResponseModel
|
||||
import org.dreamexposure.discal.core.`object`.new.model.google.OauthV4RefreshTokenResponse
|
||||
import org.dreamexposure.discal.core.utils.GlobalVal.DEFAULT
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class GoogleAuthApiWrapper(
|
||||
private val apiWrapperClient: ApiWrapperClient,
|
||||
) {
|
||||
suspend fun refreshAccessToken(refreshToken: String): ResponseModel<OauthV4RefreshTokenResponse> {
|
||||
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 = apiWrapperClient.makeRequest(request, OauthV4RefreshTokenResponse::class.java)
|
||||
|
||||
|
||||
// TODO: Handling of this should be moved up higher in the impl?
|
||||
if (response.error?.error == "invalid_grant") {
|
||||
LOGGER.debug(DEFAULT, "Google Oauth invalid_grant for access token refresh")
|
||||
throw AccessRevokedException() // TODO: How should I handle this for external calendars? Right now we just delete everything
|
||||
} else if (response.error != null) {
|
||||
LOGGER.error(DEFAULT, "[Google] Error requesting new access token | ${response.code} | ${response.error.error}")
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
package org.dreamexposure.discal.core.business.google
|
||||
|
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential
|
||||
import com.google.api.client.googleapis.json.GoogleJsonResponseException
|
||||
import com.google.api.client.http.javanet.NetHttpTransport
|
||||
import com.google.api.client.json.gson.GsonFactory
|
||||
import com.google.api.client.util.DateTime
|
||||
import com.google.api.services.calendar.model.AclRule
|
||||
import com.google.api.services.calendar.model.Calendar
|
||||
import com.google.api.services.calendar.model.CalendarListEntry
|
||||
import com.google.api.services.calendar.model.Event
|
||||
import discord4j.common.util.Snowflake
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.dreamexposure.discal.CalendarTokenCache
|
||||
import org.dreamexposure.discal.core.business.api.CamApiWrapper
|
||||
import org.dreamexposure.discal.core.config.Config
|
||||
import org.dreamexposure.discal.core.enums.calendar.CalendarHost
|
||||
import org.dreamexposure.discal.core.extensions.asSnowflake
|
||||
import org.dreamexposure.discal.core.extensions.isExpiredTtl
|
||||
import org.dreamexposure.discal.core.logger.LOGGER
|
||||
import org.dreamexposure.discal.core.`object`.new.CalendarMetadata
|
||||
import org.dreamexposure.discal.core.`object`.new.model.ResponseModel
|
||||
import org.dreamexposure.discal.core.`object`.rest.ErrorResponse
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.stereotype.Component
|
||||
import java.time.Instant
|
||||
import com.google.api.services.calendar.Calendar as GoogleCalendarService
|
||||
|
||||
|
||||
@Component
|
||||
class GoogleCalendarApiWrapper(
|
||||
private val camApiWrapper: CamApiWrapper,
|
||||
private val calendarTokenCache: CalendarTokenCache, // Technically an antipattern, but it is domain-specific so...
|
||||
) {
|
||||
// Using this as guildId for caching discal owned credentials with guild owned ones cuz its efficient
|
||||
private val discalId = Config.DISCORD_APP_ID.getLong().asSnowflake()
|
||||
|
||||
/////////
|
||||
/// Auth
|
||||
/////////
|
||||
|
||||
/* Get access token to calendar from cache or CAM */
|
||||
private suspend fun getAccessToken(credentialId: Int): String {
|
||||
val token = calendarTokenCache.get(guildId = discalId, credentialId)
|
||||
if (token != null && !token.validUntil.isExpiredTtl()) return token.accessToken
|
||||
|
||||
LOGGER.debug("Fetching new local-copy of global google calendar token via CAM | credentialId:$credentialId")
|
||||
|
||||
val tokenResponse = camApiWrapper.getCalendarToken(credentialId)
|
||||
return if (tokenResponse.entity != null) {
|
||||
calendarTokenCache.put(guildId = discalId, credentialId, tokenResponse.entity)
|
||||
tokenResponse.entity.accessToken
|
||||
} else {
|
||||
throw RuntimeException("Error requesting local google calendar token from CAM for credentialId: $credentialId | response: error: ${tokenResponse.error?.error}")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getAccessToken(guildId: Snowflake, calendarNumber: Int): String {
|
||||
val token = calendarTokenCache.get(guildId, calendarNumber)
|
||||
if (token != null && !token.validUntil.isExpiredTtl()) return token.accessToken
|
||||
|
||||
LOGGER.debug("Fetching new local-copy of external google calendar token via CAM | guild:${guildId.asLong()} calendarNumber:$calendarNumber")
|
||||
|
||||
val tokenResponse = camApiWrapper.getCalendarToken(guildId, calendarNumber, CalendarHost.GOOGLE)
|
||||
return if (tokenResponse.entity != null) {
|
||||
calendarTokenCache.put(guildId, calendarNumber, tokenResponse.entity)
|
||||
tokenResponse.entity.accessToken
|
||||
} else if (tokenResponse.code == HttpStatus.FORBIDDEN.value() && tokenResponse.error?.error.equals("Access to resource revoked")) {
|
||||
// User MUST reauthorize DisCal in Google if we are seeing this error as the refresh token is invalid
|
||||
// TODO: Call to delete calendar here to mimic old behavior. Consider marking instead so they can re-auth?
|
||||
throw NotImplementedError("Call to delete calendar on refresh token revoked (or alternative) not yet implemented")
|
||||
} else {
|
||||
throw RuntimeException("Error requesting local google calendar token from CAM for guild:${guildId.asString()} calendarNumber: $calendarNumber | response: error: ${tokenResponse.error?.error}")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun buildGoogleCalendarService(accessToken: String): GoogleCalendarService {
|
||||
val credential = GoogleCredential().setAccessToken(accessToken)
|
||||
|
||||
return GoogleCalendarService.Builder(NetHttpTransport(), GsonFactory.getDefaultInstance(), credential)
|
||||
.setApplicationName("DisCal")
|
||||
.build()
|
||||
}
|
||||
|
||||
private suspend fun getGoogleCalendarService(credentialId: Int) = buildGoogleCalendarService(getAccessToken(credentialId))
|
||||
|
||||
private suspend fun getGoogleCalendarService(guildId: Snowflake, calendarNumber: Int) = buildGoogleCalendarService(getAccessToken(guildId, calendarNumber))
|
||||
|
||||
private suspend fun getGoogleCalendarService(calendarMetadata: CalendarMetadata): GoogleCalendarService {
|
||||
return if (calendarMetadata.external) getGoogleCalendarService(calendarMetadata.guildId, calendarMetadata.number)
|
||||
else getGoogleCalendarService(calendarMetadata.secrets.credentialId)
|
||||
}
|
||||
|
||||
/////////
|
||||
/// ACL Rule
|
||||
/////////
|
||||
suspend fun insertAclRule(rule: AclRule, calMetadata: CalendarMetadata): ResponseModel<AclRule> = withContext(Dispatchers.IO) {
|
||||
val service = getGoogleCalendarService(calMetadata)
|
||||
|
||||
try {
|
||||
val aclRule = service.acl()
|
||||
.insert(calMetadata.id, rule)
|
||||
.setQuotaUser(calMetadata.guildId.asString())
|
||||
.execute()
|
||||
ResponseModel(aclRule)
|
||||
} catch (e: Exception) {
|
||||
LOGGER.error("Failed to insert ACL rule for Google Calendar", e)
|
||||
ResponseModel(ErrorResponse("Failed to insert ACL rule for Google Calendar", e), 600)
|
||||
}
|
||||
}
|
||||
|
||||
/////////
|
||||
/// Calendars
|
||||
/////////
|
||||
suspend fun createCalendar(calendar: Calendar, credentialId: Int, guildId: Snowflake): ResponseModel<Calendar> = withContext(Dispatchers.IO) {
|
||||
val service = getGoogleCalendarService(credentialId)
|
||||
|
||||
try {
|
||||
val calendar = service.calendars()
|
||||
.insert(calendar)
|
||||
.setQuotaUser(guildId.asString())
|
||||
.execute()
|
||||
ResponseModel(calendar)
|
||||
} catch (e: Exception) {
|
||||
LOGGER.error("Failed to create calendar for Google Calendar", e)
|
||||
ResponseModel(ErrorResponse("Failed to create calendar for Google Calendar", e), 600)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun patchCalendar(calendar: Calendar, metadata: CalendarMetadata): ResponseModel<Calendar> = withContext(Dispatchers.IO) {
|
||||
val service = getGoogleCalendarService(metadata)
|
||||
|
||||
try {
|
||||
val calendar = service.calendars()
|
||||
.patch(calendar.id, calendar)
|
||||
.setQuotaUser(metadata.guildId.asString())
|
||||
.execute()
|
||||
ResponseModel(calendar)
|
||||
} catch (e: Exception) {
|
||||
LOGGER.error("Failed to patch calendar for Google Calendar", e)
|
||||
ResponseModel(ErrorResponse("Failed to patch calendar for Google Calendar", e), 600)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateCalendar(calendar: Calendar, metadata: CalendarMetadata): ResponseModel<Calendar> = withContext(Dispatchers.IO) {
|
||||
val service = getGoogleCalendarService(metadata)
|
||||
|
||||
try {
|
||||
val calendar = service.calendars()
|
||||
.patch(calendar.id, calendar)
|
||||
.setQuotaUser(metadata.guildId.asString())
|
||||
.execute()
|
||||
ResponseModel(calendar)
|
||||
} catch (e: Exception){
|
||||
LOGGER.error("Failed to update calendar for Google Calendar", e)
|
||||
ResponseModel(ErrorResponse("Failed to update calendar for Google Calendar", e), 600)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getCalendar(metadata: CalendarMetadata): ResponseModel<Calendar> = withContext(Dispatchers.IO) {
|
||||
val service = getGoogleCalendarService(metadata)
|
||||
|
||||
try {
|
||||
val calendar= service.calendars()
|
||||
.get(metadata.address)
|
||||
.setQuotaUser(metadata.guildId.asString())
|
||||
.execute()
|
||||
ResponseModel(calendar)
|
||||
} catch (e: Exception) {
|
||||
LOGGER.error("Failed to get calendar from Google Calendar", e)
|
||||
ResponseModel(ErrorResponse("Failed to get calendar from Google Calendar", e), 600)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteCalendar(metadata: CalendarMetadata): ResponseModel<Boolean> = withContext(Dispatchers.IO) {
|
||||
// Sanity check if calendar can be deleted
|
||||
if (metadata.external || metadata.address.equals("primary", true)) return@withContext ResponseModel(false)
|
||||
|
||||
val service = getGoogleCalendarService(metadata)
|
||||
|
||||
try {
|
||||
service.calendars()
|
||||
.delete(metadata.address)
|
||||
.setQuotaUser(metadata.guildId.asString())
|
||||
.execute()
|
||||
ResponseModel(true)
|
||||
} catch (e: Exception) {
|
||||
LOGGER.error("Failed to delete calendar from Google Calendar", e)
|
||||
ResponseModel(600, false, ErrorResponse("Failed to delete calendar from Google Calendar", e))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getUsersExternalCalendars(metadata: CalendarMetadata): ResponseModel<List<CalendarListEntry>> = withContext(Dispatchers.IO) {
|
||||
val service = getGoogleCalendarService(metadata)
|
||||
|
||||
try {
|
||||
val calendarList = service.calendarList()
|
||||
.list()
|
||||
.setMinAccessRole("writer")
|
||||
.setQuotaUser(metadata.guildId.asString())
|
||||
.execute()
|
||||
ResponseModel(calendarList.items)
|
||||
} catch (e: Exception) {
|
||||
LOGGER.error("Failed to list external calendars", e)
|
||||
ResponseModel(600, emptyList(), ErrorResponse("Failed to list external calendars", e))
|
||||
}
|
||||
}
|
||||
|
||||
/////////
|
||||
/// Events
|
||||
/////////
|
||||
suspend fun createEvent(metadata: CalendarMetadata, event: Event): ResponseModel<Event> = withContext(Dispatchers.IO) {
|
||||
val service = getGoogleCalendarService(metadata)
|
||||
|
||||
try {
|
||||
val event = service.events()
|
||||
.insert(metadata.id, event)
|
||||
.setQuotaUser(metadata.guildId.asString())
|
||||
.execute()
|
||||
ResponseModel(event)
|
||||
} catch (e: Exception) {
|
||||
LOGGER.error("Failed to create event on Google Calendar", e)
|
||||
ResponseModel(ErrorResponse("Failed to create event on Google Calendar", e), 600)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun patchEvent(metadata: CalendarMetadata, event: Event): ResponseModel<Event> = withContext(Dispatchers.IO) {
|
||||
val service = getGoogleCalendarService(metadata)
|
||||
|
||||
try {
|
||||
val event = service.events()
|
||||
.patch(metadata.id, event.id, event)
|
||||
.setQuotaUser(metadata.guildId.asString())
|
||||
.execute()
|
||||
ResponseModel(event)
|
||||
} catch (e: Exception) {
|
||||
LOGGER.error("Failed to patch event on Google Calendar", e)
|
||||
ResponseModel(ErrorResponse("Failed to patch event on Google Calendar", e), 600)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateEvent(metadata: CalendarMetadata, event: Event): ResponseModel<Event> = withContext(Dispatchers.IO) {
|
||||
val service = getGoogleCalendarService(metadata)
|
||||
|
||||
try {
|
||||
val event = service.events()
|
||||
.update(metadata.id, event.id, event)
|
||||
.setQuotaUser(metadata.guildId.asString())
|
||||
.execute()
|
||||
ResponseModel(event)
|
||||
} catch (e: Exception) {
|
||||
LOGGER.error("Failed to update event on Google Calendar", e)
|
||||
ResponseModel(ErrorResponse("Failed to update event on Google Calendar", e), 600)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getEvent(metadata: CalendarMetadata, id: String): ResponseModel<Event> = withContext(Dispatchers.IO) {
|
||||
val service = getGoogleCalendarService(metadata)
|
||||
|
||||
// This whole block can probably be shortened once old impl behavior is moved to a higher abstraction layer
|
||||
try {
|
||||
val event = service.events()
|
||||
.get(metadata.id, id)
|
||||
.setQuotaUser(metadata.guildId.asString())
|
||||
.execute()
|
||||
|
||||
if (event.status.equals("cancelled", true)) ResponseModel(404, null, null)
|
||||
else ResponseModel(event)
|
||||
} catch (e: GoogleJsonResponseException) {
|
||||
when (HttpStatus.valueOf(e.statusCode)) {
|
||||
HttpStatus.GONE -> {
|
||||
// Event is gone. Sometimes Google will return this if the event is deleted
|
||||
ResponseModel(404, null, ErrorResponse("Event Deleted"))
|
||||
}
|
||||
HttpStatus.NOT_FOUND -> {
|
||||
// Event not found. Was this ever an event?
|
||||
ResponseModel(404, null, ErrorResponse("Event Not Found"))
|
||||
} else -> {
|
||||
LOGGER.error("Failed to get event on Google Calendar w/ GoogleResponseException", e)
|
||||
ResponseModel(ErrorResponse("Failed to get event on Google Calendar w/ GoogleResponseException", e), e.statusCode)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LOGGER.error("Failed to get event from Google Calendar", e)
|
||||
ResponseModel(ErrorResponse("Failed to get event from Google Calendar", e), 600)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getEvents(metadata: CalendarMetadata, amount: Int, start: Instant): ResponseModel<List<Event>> = withContext(Dispatchers.IO) {
|
||||
val service = getGoogleCalendarService(metadata)
|
||||
|
||||
try {
|
||||
val events = service.events()
|
||||
.list(metadata.id)
|
||||
.setMaxResults(amount)
|
||||
.setTimeMin(DateTime(start.toEpochMilli()))
|
||||
.setOrderBy("startTime")
|
||||
.setSingleEvents(true)
|
||||
.setShowDeleted(false)
|
||||
.setQuotaUser(metadata.guildId.asString())
|
||||
.execute()
|
||||
ResponseModel(events.items)
|
||||
} catch (e: Exception) {
|
||||
LOGGER.error("Failed to get events from Google Calendar by start date (variant 1)", e)
|
||||
ResponseModel(600, emptyList(), ErrorResponse("Failed to get events from Google Calendar", e))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getEvents(metadata: CalendarMetadata, amount: Int, start: Instant, end: Instant): ResponseModel<List<Event>> = withContext(Dispatchers.IO) {
|
||||
val service = getGoogleCalendarService(metadata)
|
||||
|
||||
try {
|
||||
val events = service.events()
|
||||
.list(metadata.id)
|
||||
.setMaxResults(amount)
|
||||
.setTimeMin(DateTime(start.toEpochMilli()))
|
||||
.setTimeMax(DateTime(end.toEpochMilli()))
|
||||
.setOrderBy("startTime")
|
||||
.setSingleEvents(true)
|
||||
.setShowDeleted(false)
|
||||
.setQuotaUser(metadata.guildId.asString())
|
||||
.execute()
|
||||
ResponseModel(events.items)
|
||||
} catch (e: Exception) {
|
||||
LOGGER.error("Failed to get events from Google Calendar by start and end date (variant 2)", e)
|
||||
ResponseModel(600, emptyList(), ErrorResponse("Failed to get events from Google Calendar", e))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getEvents(metadata: CalendarMetadata, start: Instant, end: Instant): ResponseModel<List<Event>> = withContext(Dispatchers.IO) {
|
||||
val service = getGoogleCalendarService(metadata)
|
||||
|
||||
try {
|
||||
val events = service.events()
|
||||
.list(metadata.id)
|
||||
.setTimeMin(DateTime(start.toEpochMilli()))
|
||||
.setTimeMax(DateTime(end.toEpochMilli()))
|
||||
.setOrderBy("startTime")
|
||||
.setSingleEvents(true)
|
||||
.setShowDeleted(false)
|
||||
.setQuotaUser(metadata.guildId.asString())
|
||||
.execute()
|
||||
ResponseModel(events.items)
|
||||
} catch (e: Exception) {
|
||||
LOGGER.error("Failed to get events from Google Calendar by start and end date without amount (variant 3)", e)
|
||||
ResponseModel(600, emptyList(), ErrorResponse("Failed to get events from Google Calendar", e))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteEvent(metadata: CalendarMetadata, id: String): ResponseModel<Boolean> = withContext(Dispatchers.IO) {
|
||||
val service = getGoogleCalendarService(metadata)
|
||||
|
||||
try {
|
||||
val response = service.events()
|
||||
.delete(metadata.id, id)
|
||||
.setQuotaUser(metadata.guildId.asString())
|
||||
.executeUnparsed()
|
||||
|
||||
//Google sends 4 possible status codes, 200, 204, 404, 410.
|
||||
// First 2 should be treated as successful, and the other 2 as not found.
|
||||
when (response.statusCode) {
|
||||
200, 204 -> ResponseModel(true)
|
||||
404, 410 -> ResponseModel(false)
|
||||
else -> {
|
||||
//Log response data and return false as google sent an unexpected response code.
|
||||
LOGGER.error("Failed to delete event from Google Calendar w/ unknown response | ${response.statusCode} | ${response.statusMessage}")
|
||||
ResponseModel(response.statusCode, false, ErrorResponse(response.statusMessage))
|
||||
}
|
||||
}
|
||||
} catch (e: GoogleJsonResponseException) {
|
||||
if (e.statusCode != 410 || e.statusCode != 404) {
|
||||
LOGGER.error("Failed to delete event from Google Calendar", e)
|
||||
ResponseModel(e.statusCode, false, ErrorResponse(e.statusMessage, e))
|
||||
} else ResponseModel(true)
|
||||
} catch (e: Exception) {
|
||||
LOGGER.error("Failed to delete event from Google Calendar w/ unknown error", e)
|
||||
ResponseModel(600, false, ErrorResponse("Failed to delete event from Google Calendar", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
package org.dreamexposure.discal.core.business.google
|
||||
|
||||
import com.google.api.client.util.DateTime
|
||||
import com.google.api.services.calendar.model.AclRule
|
||||
import com.google.api.services.calendar.model.EventDateTime
|
||||
import discord4j.common.util.Snowflake
|
||||
import org.dreamexposure.discal.core.business.CalendarProvider
|
||||
import org.dreamexposure.discal.core.business.EventMetadataService
|
||||
import org.dreamexposure.discal.core.config.Config
|
||||
import org.dreamexposure.discal.core.crypto.KeyGenerator
|
||||
import org.dreamexposure.discal.core.enums.event.EventColor
|
||||
import org.dreamexposure.discal.core.exceptions.ApiException
|
||||
import org.dreamexposure.discal.core.extensions.google.asInstant
|
||||
import org.dreamexposure.discal.core.`object`.event.Recurrence
|
||||
import org.dreamexposure.discal.core.`object`.new.Calendar
|
||||
import org.dreamexposure.discal.core.`object`.new.CalendarMetadata
|
||||
import org.dreamexposure.discal.core.`object`.new.Event
|
||||
import org.dreamexposure.discal.core.`object`.new.EventMetadata
|
||||
import org.springframework.stereotype.Component
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.temporal.ChronoUnit
|
||||
import kotlin.random.Random
|
||||
import com.google.api.services.calendar.model.Event as GoogleEvent
|
||||
|
||||
@Component
|
||||
class GoogleCalendarProviderService(
|
||||
val googleCalendarApiWrapper: GoogleCalendarApiWrapper,
|
||||
val eventMetadataService: EventMetadataService,
|
||||
) : CalendarProvider {
|
||||
override val host = CalendarMetadata.Host.GOOGLE
|
||||
|
||||
/////////
|
||||
/// Calendar
|
||||
/////////
|
||||
override suspend fun getCalendar(metadata: CalendarMetadata): Calendar? {
|
||||
val response = googleCalendarApiWrapper.getCalendar(metadata)
|
||||
if (response.entity == null) return null
|
||||
|
||||
return Calendar(
|
||||
metadata = metadata,
|
||||
name = response.entity.summary.orEmpty(),
|
||||
description = response.entity.description.orEmpty(),
|
||||
timezone = ZoneId.of(response.entity.timeZone),
|
||||
hostLink = "https://calendar.google.com/calendar/embed?src=${metadata.id}"
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun createCalendar(guildId: Snowflake, spec: Calendar.CreateSpec): Calendar {
|
||||
val credentialId = randomCredentialId()
|
||||
val googleCalendar = com.google.api.services.calendar.model.Calendar()
|
||||
|
||||
googleCalendar.summary = spec.name
|
||||
googleCalendar.description = spec.description
|
||||
googleCalendar.timeZone = spec.timezone.id
|
||||
|
||||
|
||||
val response = googleCalendarApiWrapper.createCalendar(googleCalendar, credentialId, guildId)
|
||||
if (response.entity == null) throw ApiException(response.error?.error, response.error?.exception)
|
||||
|
||||
val metadata = CalendarMetadata(
|
||||
guildId = guildId,
|
||||
number = spec.number,
|
||||
host = CalendarMetadata.Host.GOOGLE,
|
||||
id = response.entity.id,
|
||||
address = response.entity.id,
|
||||
external = false,
|
||||
secrets = CalendarMetadata.Secrets(
|
||||
credentialId = credentialId,
|
||||
privateKey = KeyGenerator.csRandomAlphaNumericString(16),
|
||||
expiresAt = Instant.now(),
|
||||
refreshToken = "",
|
||||
accessToken = "",
|
||||
)
|
||||
)
|
||||
|
||||
// Add required ACL rule
|
||||
val aclRuleResponse = googleCalendarApiWrapper.insertAclRule(
|
||||
AclRule().setScope(AclRule.Scope().setType("default")).setRole("reader"),
|
||||
metadata
|
||||
)
|
||||
if (aclRuleResponse.error != null) throw ApiException(
|
||||
aclRuleResponse.error.error,
|
||||
aclRuleResponse.error.exception
|
||||
)
|
||||
|
||||
return Calendar(
|
||||
metadata = metadata,
|
||||
name = response.entity.summary.orEmpty(),
|
||||
description = response.entity.description.orEmpty(),
|
||||
timezone = ZoneId.of(response.entity.timeZone),
|
||||
hostLink = "https://calendar.google.com/calendar/embed?src=${response.entity.id}",
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun updateCalendar(guildId: Snowflake, metadata: CalendarMetadata, spec: Calendar.UpdateSpec): Calendar {
|
||||
val content = com.google.api.services.calendar.model.Calendar()
|
||||
|
||||
spec.name?.let { content.summary = it }
|
||||
spec.description?.let { content.description = it }
|
||||
spec.timezone?.let { content.timeZone = it.id }
|
||||
|
||||
val response = googleCalendarApiWrapper.patchCalendar(content, metadata)
|
||||
if (response.entity == null) throw ApiException(response.error?.error, response.error?.exception)
|
||||
|
||||
// Add required ACL rule
|
||||
val aclRuleResponse = googleCalendarApiWrapper.insertAclRule(
|
||||
AclRule().setScope(AclRule.Scope().setType("default")).setRole("reader"),
|
||||
metadata
|
||||
)
|
||||
if (aclRuleResponse.error != null) throw ApiException(
|
||||
aclRuleResponse.error.error,
|
||||
aclRuleResponse.error.exception
|
||||
)
|
||||
|
||||
return Calendar(
|
||||
metadata = metadata,
|
||||
name = response.entity.summary.orEmpty(),
|
||||
description = response.entity.description,
|
||||
timezone = ZoneId.of(response.entity.timeZone),
|
||||
hostLink = "https://calendar.google.com/calendar/embed?src=${response.entity.id}",
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun deleteCalendar(guildId: Snowflake, metadata: CalendarMetadata) {
|
||||
val response = googleCalendarApiWrapper.deleteCalendar(metadata)
|
||||
if (response.error != null) throw ApiException(response.error.error, response.error.exception)
|
||||
}
|
||||
|
||||
/////////
|
||||
/// Events
|
||||
/////////
|
||||
override suspend fun getEvent(calendar: Calendar, id: String): Event? {
|
||||
val response = googleCalendarApiWrapper.getEvent(calendar.metadata, id)
|
||||
if (response.code != 404 && response.code != 200) throw ApiException(response.error?.error, response.error?.exception)
|
||||
if (response.entity == null) return null
|
||||
val baseEvent = response.entity
|
||||
|
||||
val metadata = eventMetadataService.getEventMetadata(calendar.metadata.guildId, id)
|
||||
?: EventMetadata(id, calendar.metadata.guildId, calendar.metadata.number)
|
||||
|
||||
return mapGoogleEventToDisCalEvent(calendar, baseEvent, metadata)
|
||||
}
|
||||
|
||||
override suspend fun getUpcomingEvents(calendar: Calendar, amount: Int): List<Event> {
|
||||
val response = googleCalendarApiWrapper.getEvents(calendar.metadata, amount, Instant.now())
|
||||
if (response.entity == null) return emptyList()
|
||||
|
||||
return loadEvents(calendar, response.entity)
|
||||
}
|
||||
|
||||
override suspend fun getOngoingEvents(calendar: Calendar): List<Event> {
|
||||
val now = Instant.now()
|
||||
val start = now.minus(14, ChronoUnit.DAYS) // 2 weeks ago
|
||||
val end = now.plus(1, ChronoUnit.DAYS) // One day from now
|
||||
|
||||
|
||||
val response = googleCalendarApiWrapper.getEvents(calendar.metadata, start, end)
|
||||
if (response.entity == null) return emptyList()
|
||||
|
||||
// Filter for only the ongoing events
|
||||
val filtered = response.entity
|
||||
.filter { it.start.asInstant(calendar.timezone).isBefore(now) }
|
||||
.filter { it.end.asInstant(calendar.timezone).isAfter(now) }
|
||||
|
||||
return loadEvents(calendar, filtered)
|
||||
}
|
||||
|
||||
override suspend fun getEventsInTimeRange(calendar: Calendar, start: Instant, end: Instant): List<Event> {
|
||||
val response = googleCalendarApiWrapper.getEvents(calendar.metadata, start, end)
|
||||
if (response.entity == null) return emptyList()
|
||||
|
||||
return loadEvents(calendar, response.entity)
|
||||
}
|
||||
|
||||
override suspend fun createEvent(calendar: Calendar, spec: Event.CreateSpec): Event {
|
||||
val event = GoogleEvent()
|
||||
event.id = KeyGenerator.generateEventId()
|
||||
event.visibility = "public"
|
||||
|
||||
event.summary = spec.name
|
||||
event.description = spec.description
|
||||
event.location = spec.location
|
||||
|
||||
event.start = EventDateTime()
|
||||
.setDateTime(DateTime(spec.start.toEpochMilli()))
|
||||
.setTimeZone(calendar.timezone.id)
|
||||
event.end = EventDateTime()
|
||||
.setDateTime(DateTime(spec.end.toEpochMilli()))
|
||||
.setTimeZone(calendar.timezone.id)
|
||||
|
||||
if (spec.color != EventColor.NONE)
|
||||
event.colorId = spec.color.id.toString()
|
||||
|
||||
if (spec.recur && spec.recurrence != null)
|
||||
event.recurrence = listOf(spec.recurrence.toRRule())
|
||||
|
||||
// Create event in google
|
||||
val response = googleCalendarApiWrapper.createEvent(calendar.metadata, event)
|
||||
if (response.error != null || response.entity == null) throw ApiException(response.error?.error, response.error?.exception)
|
||||
|
||||
// Create and save metadata
|
||||
val metadata = eventMetadataService.createEventMetadata(EventMetadata(
|
||||
id = event.id,
|
||||
guildId = calendar.metadata.guildId,
|
||||
calendarNumber = calendar.metadata.number,
|
||||
eventEnd = spec.end,
|
||||
imageLink = spec.image.orEmpty(),
|
||||
))
|
||||
|
||||
return mapGoogleEventToDisCalEvent(calendar, response.entity, metadata)
|
||||
}
|
||||
|
||||
override suspend fun updateEvent(calendar: Calendar, spec: Event.UpdateSpec): Event {
|
||||
val event = GoogleEvent()
|
||||
event.id = spec.id
|
||||
|
||||
event.summary = spec.name
|
||||
event.description = spec.description
|
||||
event.location = spec.location
|
||||
|
||||
// Always update start/end so that we can safely handle all day events without DateTime by overwriting it
|
||||
event.start = EventDateTime()
|
||||
.setDateTime(DateTime(spec.start.toEpochMilli()))
|
||||
.setTimeZone(calendar.timezone.id)
|
||||
event.end = EventDateTime()
|
||||
.setDateTime(DateTime(spec.end.toEpochMilli()))
|
||||
.setTimeZone(calendar.timezone.id)
|
||||
|
||||
if (spec.color == EventColor.NONE)
|
||||
event.colorId = null
|
||||
else
|
||||
event.colorId = spec.color.id.toString()
|
||||
|
||||
// Special recurrence handling
|
||||
if (spec.recur != null) {
|
||||
if (spec.recur) {
|
||||
//event now recurs, add the RRUle.
|
||||
spec.recurrence?.let { event.recurrence = listOf(it.toRRule()) }
|
||||
}
|
||||
} else {
|
||||
//Recur equals null, so it's not changing whether its recurring, so handle if RRule changes only
|
||||
spec.recurrence?.let { event.recurrence = listOf(it.toRRule()) }
|
||||
}
|
||||
|
||||
// Okay, all values are set, let's patch this event now
|
||||
val response = googleCalendarApiWrapper.patchEvent(calendar.metadata, event)
|
||||
if (response.error != null || response.entity == null) throw ApiException(response.error?.error, response.error?.exception)
|
||||
|
||||
// Upsert metadata
|
||||
val metadata = eventMetadataService.getEventMetadata(calendar.metadata.guildId, event.id)
|
||||
?: EventMetadata(
|
||||
id = event.id,
|
||||
guildId = calendar.metadata.guildId,
|
||||
calendarNumber = calendar.metadata.number,
|
||||
eventEnd = spec.end,
|
||||
imageLink = spec.image.orEmpty(),
|
||||
)
|
||||
eventMetadataService.upsertEventMetadata(metadata)
|
||||
|
||||
return mapGoogleEventToDisCalEvent(calendar, response.entity, metadata)
|
||||
}
|
||||
|
||||
override suspend fun deleteEvent(calendar: Calendar, id: String) {
|
||||
val response = googleCalendarApiWrapper.deleteEvent(calendar.metadata, id)
|
||||
if (response.error != null) throw ApiException(response.error.error, response.error.exception)
|
||||
}
|
||||
|
||||
/////////
|
||||
/// Private util functions
|
||||
/////////
|
||||
private fun randomCredentialId() = Random.nextInt(Config.SECRET_GOOGLE_CREDENTIAL_COUNT.getInt())
|
||||
|
||||
private suspend fun loadEvents(calendar: Calendar, events: List<GoogleEvent>): List<Event> {
|
||||
val metadataList = eventMetadataService.getMultipleEventsMetadata(calendar.metadata.guildId, events.map { it.id})
|
||||
|
||||
return events.map { googleEvent ->
|
||||
val computedId = googleEvent.id.split("_")[0]
|
||||
val metadata = metadataList.firstOrNull { it.id == computedId }
|
||||
?: EventMetadata(googleEvent.id, calendar.metadata.guildId, calendar.metadata.number)
|
||||
|
||||
mapGoogleEventToDisCalEvent(calendar, googleEvent, metadata)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapGoogleEventToDisCalEvent(calendar: Calendar, baseEvent: GoogleEvent, metadata: EventMetadata): Event {
|
||||
return Event(
|
||||
id = baseEvent.id,
|
||||
guildId = calendar.metadata.guildId,
|
||||
calendarNumber = calendar.metadata.number,
|
||||
name = baseEvent.summary.orEmpty(),
|
||||
description = baseEvent.description.orEmpty(),
|
||||
location = baseEvent.location.orEmpty(),
|
||||
link = baseEvent.htmlLink.orEmpty(),
|
||||
color = if (!baseEvent.colorId.isNullOrBlank()) {
|
||||
EventColor.fromNameOrHexOrId(baseEvent.colorId)
|
||||
} else EventColor.NONE,
|
||||
start = if (baseEvent.start.dateTime != null) {
|
||||
Instant.ofEpochMilli(baseEvent.start.dateTime.value)
|
||||
} else Instant.ofEpochMilli(baseEvent.start.date.value)
|
||||
.plus(1, ChronoUnit.DAYS)
|
||||
.atZone(calendar.timezone)
|
||||
.truncatedTo(ChronoUnit.DAYS)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
.atZone(calendar.timezone)
|
||||
.toInstant(),
|
||||
end = if (baseEvent.end.dateTime != null) {
|
||||
Instant.ofEpochMilli(baseEvent.end.dateTime.value)
|
||||
} else Instant.ofEpochMilli(baseEvent.end.date.value)
|
||||
.plus(1, ChronoUnit.DAYS)
|
||||
.atZone(calendar.timezone)
|
||||
.truncatedTo(ChronoUnit.DAYS)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
.atZone(calendar.timezone)
|
||||
.toInstant(),
|
||||
recur = !baseEvent.recurrence.isNullOrEmpty(),
|
||||
recurrence = if (baseEvent.recurrence.isNullOrEmpty()) Recurrence() else Recurrence.fromRRule(baseEvent.recurrence[0]),
|
||||
image = metadata.imageLink,
|
||||
timezone = calendar.timezone,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,13 @@ package org.dreamexposure.discal.core.cache
|
||||
|
||||
import discord4j.common.util.Snowflake
|
||||
import java.time.Duration
|
||||
import kotlin.random.Random
|
||||
|
||||
interface CacheRepository<K, V> {
|
||||
val ttl: Duration
|
||||
get() = Duration.ofMinutes(60)
|
||||
val ttlWithJitter: Duration
|
||||
get() = ttl.plusSeconds(Random.Default.nextLong(-333,333))
|
||||
|
||||
// Write
|
||||
suspend fun put(guildId: Snowflake? = null, key: K, value: V)
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package org.dreamexposure.discal.core.cache
|
||||
|
||||
import discord4j.common.util.Snowflake
|
||||
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<Snowflake, GuildSettings> = ConcurrentHashMap()
|
||||
//guild id -> cal num -> calendar
|
||||
private val calendars: MutableMap<Snowflake, ConcurrentHashMap<Int, Calendar>> = ConcurrentHashMap()
|
||||
|
||||
init {
|
||||
//Automatically clear caches every so often...
|
||||
Flux.interval(Duration.ofMinutes(15))
|
||||
.doOnEach { invalidateAll() }
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
|
||||
fun invalidateAll() {
|
||||
guildSettings.clear()
|
||||
calendars.clear()
|
||||
}
|
||||
|
||||
//Functions to stop direct modification
|
||||
fun getCalendar(guildId: Snowflake, calNum: Int): Calendar? = calendars[guildId]?.get(calNum)
|
||||
|
||||
fun getAllCalendars(guildId: Snowflake): Collection<Calendar>? = calendars[guildId]?.values
|
||||
|
||||
fun putCalendar(calendar: Calendar) {
|
||||
if (calendars.containsKey(calendar.guildId))
|
||||
calendars[calendar.guildId]!![calendar.calendarNumber] = calendar
|
||||
else {
|
||||
val map = ConcurrentHashMap<Int, Calendar>()
|
||||
map[calendar.calendarNumber] = calendar
|
||||
|
||||
calendars[calendar.guildId] = map
|
||||
}
|
||||
}
|
||||
|
||||
fun handleCalendarDelete(guildId: Snowflake) {
|
||||
removeCalendars(guildId)
|
||||
//Eventually other cached things will be here, like events, rsvp data, etc
|
||||
}
|
||||
|
||||
fun removeCalendars(guildId: Snowflake) {
|
||||
calendars.remove(guildId)
|
||||
}
|
||||
}
|
||||
@@ -19,14 +19,14 @@ class JdkCacheRepository<K : Any, V>(override val ttl: Duration) : CacheReposito
|
||||
override suspend fun put(guildId: Snowflake?, key: K, value: V) {
|
||||
val guildedMap = getGuildedCache(guildId)
|
||||
|
||||
guildedMap[key] = Pair(Instant.now().plus(ttl), value)
|
||||
guildedMap[key] = Pair(Instant.now().plus(ttlWithJitter), value)
|
||||
cache[guildId] = guildedMap
|
||||
}
|
||||
|
||||
override suspend fun putMany(guildId: Snowflake?, values: Map<K, V>) {
|
||||
val guildedMap = getGuildedCache(guildId)
|
||||
|
||||
guildedMap.putAll(values.mapValues { Pair(Instant.now().plus(ttl), it.value) })
|
||||
guildedMap.putAll(values.mapValues { Pair(Instant.now().plus(ttlWithJitter), it.value) })
|
||||
cache[guildId] = guildedMap
|
||||
}
|
||||
|
||||
|
||||
@@ -25,12 +25,12 @@ class RedisStringCacheRepository<K, V>(
|
||||
}
|
||||
|
||||
override suspend fun put(guildId: Snowflake?, key: K, value: V) {
|
||||
valueOps.setAndAwait(formatKey(guildId, key), objectMapper.writeValueAsString(value), ttl)
|
||||
valueOps.setAndAwait(formatKey(guildId, key), objectMapper.writeValueAsString(value), ttlWithJitter)
|
||||
}
|
||||
|
||||
override suspend fun putMany(guildId: Snowflake?, values: Map<K, V>) {
|
||||
values.forEach { (key, value) ->
|
||||
valueOps.setAndAwait(formatKey(guildId, key), objectMapper.writeValueAsString(value), ttl)
|
||||
valueOps.setAndAwait(formatKey(guildId, key), objectMapper.writeValueAsString(value), ttlWithJitter)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ 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 io.micrometer.core.instrument.binder.okhttp3.OkHttpObservationInterceptor
|
||||
import io.micrometer.observation.ObservationRegistry
|
||||
import okhttp3.OkHttpClient
|
||||
import org.dreamexposure.discal.core.serializers.DurationMapper
|
||||
import org.dreamexposure.discal.core.serializers.SnowflakeMapper
|
||||
@@ -31,7 +33,14 @@ class BeanConfig {
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun httpClient(): OkHttpClient {
|
||||
return OkHttpClient()
|
||||
fun httpClient(registry: ObservationRegistry): OkHttpClient {
|
||||
val interceptor = OkHttpObservationInterceptor.builder(registry, "okhttp.requests")
|
||||
// This can lead to tag cardinality explosion as it doesn't use uri patterns, should investigate options for that one day
|
||||
.uriMapper { it.url.encodedPath }
|
||||
.build()
|
||||
|
||||
return OkHttpClient.Builder()
|
||||
.addInterceptor(interceptor)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.springframework.data.redis.core.ReactiveStringRedisTemplate
|
||||
|
||||
@Configuration
|
||||
class CacheConfig {
|
||||
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()
|
||||
@@ -20,9 +21,17 @@ class CacheConfig {
|
||||
private val staticMessageTtl = Config.CACHE_TTL_STATIC_MESSAGE_MINUTES.getLong().asMinutes()
|
||||
private val announcementTll = Config.CACHE_TTL_ANNOUNCEMENT_MINUTES.getLong().asMinutes()
|
||||
private val wizardTtl = Config.TIMING_WIZARD_TIMEOUT_MINUTES.getLong().asMinutes()
|
||||
private val calendarTokenTtl = Config.CACHE_TTL_CALENDAR_TOKEN_MINUTES.getLong().asMinutes()
|
||||
private val eventTtl = Config.CACHE_TTL_EVENTS_MINUTES.getLong().asMinutes()
|
||||
|
||||
|
||||
// Redis caching
|
||||
@Bean
|
||||
@Primary
|
||||
@ConditionalOnProperty("bot.cache.redis", havingValue = "true")
|
||||
fun guildSettingsRedisCache(objectMapper: ObjectMapper, redisTemplate: ReactiveStringRedisTemplate): GuildSettingsCache =
|
||||
RedisStringCacheRepository(objectMapper, redisTemplate, "GuildSettings", settingsTtl)
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
@ConditionalOnProperty("bot.cache.redis", havingValue = "true")
|
||||
@@ -35,6 +44,12 @@ class CacheConfig {
|
||||
fun oauthStateRedisCache(objectMapper: ObjectMapper, redisTemplate: ReactiveStringRedisTemplate): OauthStateCache =
|
||||
RedisStringCacheRepository(objectMapper, redisTemplate, "OauthStates", oauthStateTtl)
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
@ConditionalOnProperty("bot.cache.redis", havingValue = "true")
|
||||
fun calendarMetadataRedisCache(objectMapper: ObjectMapper, redisTemplate: ReactiveStringRedisTemplate): CalendarMetadataCache =
|
||||
RedisStringCacheRepository(objectMapper, redisTemplate, "CalendarMetadata", calendarTtl)
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
@ConditionalOnProperty("bot.cache.redis", havingValue = "true")
|
||||
@@ -65,14 +80,38 @@ class CacheConfig {
|
||||
fun announcementWizardRedisCache(objectMapper: ObjectMapper, redisTemplate: ReactiveStringRedisTemplate): AnnouncementWizardStateCache =
|
||||
RedisStringCacheRepository(objectMapper, redisTemplate, "AnnouncementWizards", wizardTtl)
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
@ConditionalOnProperty("bot.cache.redis", havingValue = "true")
|
||||
fun eventWizardRedisCache(objectMapper: ObjectMapper, redisTemplate: ReactiveStringRedisTemplate): EventWizardStateCache =
|
||||
RedisStringCacheRepository(objectMapper, redisTemplate, "EventWizards", wizardTtl)
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
@ConditionalOnProperty("bot.cache.redis", havingValue = "true")
|
||||
fun calendarWizardRedisCache(objectMapper: ObjectMapper, redisTemplate: ReactiveStringRedisTemplate): CalendarWizardStateCache =
|
||||
RedisStringCacheRepository(objectMapper, redisTemplate, "CalendarWizards", wizardTtl)
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
@ConditionalOnProperty("bot.cache.redis", havingValue = "true")
|
||||
fun eventRedisCache(objectMapper: ObjectMapper, redisTemplate: ReactiveStringRedisTemplate): EventCache =
|
||||
RedisStringCacheRepository(objectMapper, redisTemplate, "Events", eventTtl)
|
||||
|
||||
|
||||
// In-memory fallback caching
|
||||
@Bean
|
||||
fun guildSettingsFallbackCache(): GuildSettingsCache = JdkCacheRepository(settingsTtl)
|
||||
|
||||
@Bean
|
||||
fun credentialsFallbackCache(): CredentialsCache = JdkCacheRepository(credentialsTll)
|
||||
|
||||
@Bean
|
||||
fun oauthStateFallbackCache(): OauthStateCache = JdkCacheRepository(oauthStateTtl)
|
||||
|
||||
@Bean
|
||||
fun calendarMetadataFallbackCache(): CalendarMetadataCache = JdkCacheRepository(calendarTtl)
|
||||
|
||||
@Bean
|
||||
fun calendarFallbackCache(): CalendarCache = JdkCacheRepository(calendarTtl)
|
||||
|
||||
@@ -87,4 +126,16 @@ class CacheConfig {
|
||||
|
||||
@Bean
|
||||
fun announcementWizardFallbackCache(): AnnouncementWizardStateCache = JdkCacheRepository(wizardTtl)
|
||||
|
||||
@Bean
|
||||
fun eventWizardFallbackCache(): EventWizardStateCache = JdkCacheRepository(wizardTtl)
|
||||
|
||||
@Bean
|
||||
fun calendarWizardFallbackCache(): CalendarWizardStateCache = JdkCacheRepository(wizardTtl)
|
||||
|
||||
@Bean
|
||||
fun calendarTokenFallbackCache(): CalendarTokenCache = JdkCacheRepository(calendarTokenTtl)
|
||||
|
||||
@Bean
|
||||
fun eventFallbackCache(): EventCache = JdkCacheRepository(eventTtl)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.dreamexposure.discal.core.config
|
||||
|
||||
import org.dreamexposure.discal.core.config.Config.entries
|
||||
import java.io.FileReader
|
||||
import java.util.*
|
||||
|
||||
@@ -29,6 +30,8 @@ enum class Config(private val key: String, private var value: Any? = null) {
|
||||
CACHE_TTL_RSVP_MINUTES("bot.cache.ttl-minutes.rsvp", 60),
|
||||
CACHE_TTL_STATIC_MESSAGE_MINUTES("bot.cache.ttl-minutes.static-messages", 60),
|
||||
CACHE_TTL_ANNOUNCEMENT_MINUTES("bot.cache.ttl-minutes.announcements", 120),
|
||||
CACHE_TTL_CALENDAR_TOKEN_MINUTES("bot.cache.ttl-minutes.calendar", 60),
|
||||
CACHE_TTL_EVENTS_MINUTES("bot.cache.ttl-minutes.event", 15),
|
||||
|
||||
// Security configuration
|
||||
|
||||
@@ -64,6 +67,7 @@ enum class Config(private val key: String, private var value: Any? = null) {
|
||||
|
||||
// UI and UX
|
||||
EMBED_RSVP_WAITLIST_DISPLAY_LENGTH("bot.ui.embed.rsvp.waitlist.length", 3),
|
||||
CALENDAR_OVERVIEW_DEFAULT_EVENT_COUNT("bot.ui.embed.calendar.overview.event-count", 15),
|
||||
|
||||
// Everything else
|
||||
SHARD_COUNT("bot.sharding.count"),
|
||||
@@ -77,7 +81,9 @@ enum class Config(private val key: String, private var value: Any? = null) {
|
||||
LOGGING_WEBHOOKS_ALL_ERRORS("bot.logging.webhooks.all-error", false),
|
||||
|
||||
INTEGRATIONS_UPDATE_BOT_LIST_SITES("bot.integrations.update-bot-sites", false),
|
||||
INTEGRATIONS_REACTOR_METRICS("bot.integrations.reactor.metrics", false),
|
||||
|
||||
ANNOUNCEMENT_PROCESS_GUILD_DEFAULT_UPCOMING_EVENTS_COUNT("bot.announcement.process-global-default-upcoming-events", 30),
|
||||
|
||||
;
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ import reactor.core.publisher.Mono
|
||||
|
||||
interface AnnouncementRepository: R2dbcRepository<AnnouncementData, String> {
|
||||
|
||||
@Query("SELECT COUNT(*) FROM announcements")
|
||||
fun countAll(): Mono<Long>
|
||||
|
||||
fun findByGuildIdAndAnnouncementId(guildId: Long, announcementId: String): Mono<AnnouncementData>
|
||||
|
||||
fun findAllByGuildId(guildId: Long): Flux<AnnouncementData>
|
||||
@@ -77,4 +80,13 @@ interface AnnouncementRepository: R2dbcRepository<AnnouncementData, String> {
|
||||
fun deleteByAnnouncementId(announcementId: String): Mono<Void>
|
||||
|
||||
fun deleteAllByGuildIdAndEventId(guildId: Long, eventId: String): Mono<Void>
|
||||
|
||||
fun deleteAllByGuildIdAndCalendarNumber(guildId: Long, calendarNumber: Int): Mono<Void>
|
||||
|
||||
@Query("""
|
||||
UPDATE announcements
|
||||
SET calendar_number = calendar_number - 1
|
||||
WHERE calendar_number >= :calendarNumber AND guild_id = :guildId
|
||||
""")
|
||||
fun decrementCalendarsByGuildIdAndCalendarNumber(guildId: Long, calendarNumber: Int): Mono<Long>
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package org.dreamexposure.discal.core.database
|
||||
import org.springframework.data.relational.core.mapping.Table
|
||||
|
||||
@Table("calendars")
|
||||
data class CalendarData(
|
||||
data class CalendarMetadataData(
|
||||
val guildId: Long,
|
||||
val calendarNumber: Int,
|
||||
val host: String,
|
||||
@@ -5,11 +5,16 @@ import org.springframework.data.r2dbc.repository.R2dbcRepository
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
interface CalendarRepository : R2dbcRepository<CalendarData, Long> {
|
||||
interface CalendarMetadataRepository : R2dbcRepository<CalendarMetadataData, Long> {
|
||||
|
||||
fun findAllByGuildId(guildId: Long): Flux<CalendarData>
|
||||
@Query("SELECT COUNT(*) FROM calendars")
|
||||
fun countAll(): Mono<Long>
|
||||
|
||||
fun findByGuildIdAndCalendarNumber(guildId: Long, calendarNumber: Int): Mono<CalendarData>
|
||||
fun countAllByGuildId(guildId: Long): Mono<Long>
|
||||
|
||||
fun findAllByGuildId(guildId: Long): Flux<CalendarMetadataData>
|
||||
|
||||
fun findByGuildIdAndCalendarNumber(guildId: Long, calendarNumber: Int): Mono<CalendarMetadataData>
|
||||
|
||||
@Query("""
|
||||
UPDATE calendars
|
||||
@@ -38,6 +43,12 @@ interface CalendarRepository : R2dbcRepository<CalendarData, Long> {
|
||||
expiresAt: Long,
|
||||
): Mono<Int>
|
||||
|
||||
@Query(" SELECT 1")
|
||||
fun healthCheck(): Mono<Int>
|
||||
fun deleteAllByGuildIdAndCalendarNumber(guildId: Long, calendarNumber: Int): Mono<Void>
|
||||
|
||||
@Query("""
|
||||
UPDATE calendars
|
||||
SET calendar_number = calendar_number - 1
|
||||
WHERE calendar_number >= :calendarNumber AND guild_id = :guildId
|
||||
""")
|
||||
fun decrementCalendarsByGuildIdAndCalendarNumber(guildId: Long, calendarNumber: Int): Mono<Long>
|
||||
}
|
||||
@@ -1,787 +0,0 @@
|
||||
@file:Suppress("DuplicatedCode")
|
||||
|
||||
package org.dreamexposure.discal.core.database
|
||||
|
||||
import discord4j.common.util.Snowflake
|
||||
import io.r2dbc.pool.ConnectionPool
|
||||
import io.r2dbc.pool.ConnectionPoolConfiguration
|
||||
import io.r2dbc.spi.Connection
|
||||
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.AnnouncementStyle
|
||||
import org.dreamexposure.discal.core.enums.calendar.CalendarHost
|
||||
import org.dreamexposure.discal.core.enums.time.TimeFormat
|
||||
import org.dreamexposure.discal.core.extensions.setFromString
|
||||
import org.dreamexposure.discal.core.logger.LOGGER
|
||||
import org.dreamexposure.discal.core.`object`.GuildSettings
|
||||
import org.dreamexposure.discal.core.`object`.calendar.CalendarData
|
||||
import org.dreamexposure.discal.core.`object`.event.EventData
|
||||
import org.dreamexposure.discal.core.`object`.web.UserAPIAccount
|
||||
import org.dreamexposure.discal.core.utils.GlobalVal.DEFAULT
|
||||
import org.intellij.lang.annotations.Language
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.util.retry.Retry
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.function.Function
|
||||
|
||||
object DatabaseManager {
|
||||
private val pool: ConnectionPool
|
||||
|
||||
init {
|
||||
val factory = ConnectionFactories.get(
|
||||
builder()
|
||||
.option(DRIVER, "pool")
|
||||
.option(PROTOCOL, "mysql")
|
||||
.from(parse(Config.SQL_URL.getString()))
|
||||
.option(USER, Config.SQL_USERNAME.getString())
|
||||
.option(PASSWORD, Config.SQL_PASSWORD.getString())
|
||||
.build()
|
||||
)
|
||||
|
||||
val conf = ConnectionPoolConfiguration.builder()
|
||||
.connectionFactory(factory)
|
||||
.maxLifeTime(Duration.ofHours(1))
|
||||
.build()
|
||||
|
||||
pool = ConnectionPool(conf)
|
||||
}
|
||||
|
||||
//FIXME: attempt to fix constant open/close of connections
|
||||
private fun <T> connect(connection: Function<Connection, Mono<T>>): Mono<T> {
|
||||
return Mono.usingWhen(pool.create(), connection::apply, Connection::close)
|
||||
}
|
||||
|
||||
fun disconnectFromMySQL() = pool.dispose()
|
||||
|
||||
fun updateAPIAccount(acc: UserAPIAccount): Mono<Boolean> {
|
||||
return connect { c ->
|
||||
Mono.from(
|
||||
c.createStatement(Queries.SELECT_API_KEY)
|
||||
.bind(0, acc.APIKey)
|
||||
.execute()
|
||||
).flatMapMany { res ->
|
||||
res.map { row, _ -> row }
|
||||
}.hasElements().flatMap { exists ->
|
||||
if (exists) {
|
||||
val updateCommand = """ UPDATE ${Tables.API} SET
|
||||
USER_ID = ?, BLOCKED = ?
|
||||
WHERE API_KEY = ?
|
||||
""".trimMargin()
|
||||
|
||||
Mono.from(
|
||||
c.createStatement(updateCommand)
|
||||
.bind(0, acc.userId)
|
||||
.bind(1, acc.blocked)
|
||||
.bind(2, acc.APIKey)
|
||||
.execute()
|
||||
).flatMap { res -> Mono.from(res.rowsUpdated) }
|
||||
.thenReturn(true)
|
||||
} else {
|
||||
val insertCommand = """INSERT INTO ${Tables.API}
|
||||
(USER_ID, API_KEY, BLOCKED, TIME_ISSUED)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""".trimMargin()
|
||||
|
||||
Mono.from(
|
||||
c.createStatement(insertCommand)
|
||||
.bind(0, acc.userId)
|
||||
.bind(1, acc.APIKey)
|
||||
.bind(2, acc.blocked)
|
||||
.bind(3, acc.timeIssued)
|
||||
.execute()
|
||||
).flatMap { res -> Mono.from(res.rowsUpdated) }
|
||||
.thenReturn(true)
|
||||
}
|
||||
}.doOnError {
|
||||
LOGGER.error(DEFAULT, "Failed to update API account", it)
|
||||
}.onErrorResume { Mono.just(false) }
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSettings(settings: GuildSettings): Mono<Boolean> {
|
||||
DiscalCache.guildSettings[settings.guildID] = settings
|
||||
|
||||
return connect { c ->
|
||||
Mono.from(
|
||||
c.createStatement(Queries.SELECT_GUILD_SETTINGS)
|
||||
.bind(0, settings.guildID.asLong())
|
||||
.execute()
|
||||
).flatMapMany { res ->
|
||||
res.map { row, _ -> row }
|
||||
}.hasElements().flatMap { exists ->
|
||||
if (exists) {
|
||||
val updateCommand = """UPDATE ${Tables.GUILD_SETTINGS} SET
|
||||
CONTROL_ROLE = ?, ANNOUNCEMENT_STYLE = ?, TIME_FORMAT = ?,
|
||||
LANG = ?, PREFIX = ?, PATRON_GUILD = ?, DEV_GUILD = ?,
|
||||
MAX_CALENDARS = ?, DM_ANNOUNCEMENTS = ?,
|
||||
BRANDED = ?, event_keep_duration = ? WHERE GUILD_ID = ?
|
||||
""".trimMargin()
|
||||
|
||||
Mono.from(
|
||||
c.createStatement(updateCommand)
|
||||
.bind(0, settings.controlRole)
|
||||
.bind(1, settings.announcementStyle.value)
|
||||
.bind(2, settings.timeFormat.value)
|
||||
.bind(3, settings.lang)
|
||||
.bind(4, settings.prefix)
|
||||
.bind(5, settings.patronGuild)
|
||||
.bind(6, settings.devGuild)
|
||||
.bind(7, settings.maxCalendars)
|
||||
.bind(8, settings.getDmAnnouncementsString())
|
||||
.bind(9, settings.branded)
|
||||
.bind(10, settings.eventKeepDuration)
|
||||
.bind(11, settings.guildID.asLong())
|
||||
.execute()
|
||||
).flatMap { res -> Mono.from(res.rowsUpdated) }
|
||||
.hasElement()
|
||||
.thenReturn(true)
|
||||
} 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, event_keep_duration)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""".trimMargin()
|
||||
|
||||
Mono.from(
|
||||
c.createStatement(insertCommand)
|
||||
.bind(0, settings.guildID.asLong())
|
||||
.bind(1, settings.controlRole)
|
||||
.bind(2, settings.announcementStyle.value)
|
||||
.bind(3, settings.timeFormat.value)
|
||||
.bind(4, settings.lang)
|
||||
.bind(5, settings.prefix)
|
||||
.bind(6, settings.patronGuild)
|
||||
.bind(7, settings.devGuild)
|
||||
.bind(8, settings.maxCalendars)
|
||||
.bind(9, settings.getDmAnnouncementsString())
|
||||
.bind(10, settings.branded)
|
||||
.bind(11, settings.eventKeepDuration)
|
||||
.execute()
|
||||
).flatMap { res -> Mono.from(res.rowsUpdated) }
|
||||
.hasElement()
|
||||
.thenReturn(true)
|
||||
}
|
||||
}.doOnError {
|
||||
LOGGER.error(DEFAULT, "Failed to update guild settings", it)
|
||||
}.onErrorResume { Mono.just(false) }
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCalendar(calData: CalendarData): Mono<Boolean> {
|
||||
return connect { c ->
|
||||
Mono.from(
|
||||
c.createStatement(Queries.SELECT_CALENDAR_BY_GUILD)
|
||||
.bind(0, calData.guildId.asLong())
|
||||
.bind(1, calData.calendarNumber)
|
||||
.execute()
|
||||
).flatMapMany { res ->
|
||||
res.map { row, _ -> row }
|
||||
}.hasElements().flatMap { exists ->
|
||||
if (exists) {
|
||||
val updateCommand = """UPDATE ${Tables.CALENDARS} SET
|
||||
HOST = ?, CALENDAR_ID = ?,
|
||||
CALENDAR_ADDRESS = ?, EXTERNAL = ?, CREDENTIAL_ID = ?,
|
||||
PRIVATE_KEY = ?, ACCESS_TOKEN = ?, REFRESH_TOKEN = ?, EXPIRES_AT = ?
|
||||
WHERE GUILD_ID = ? AND CALENDAR_NUMBER = ?
|
||||
""".trimMargin()
|
||||
|
||||
Mono.from(
|
||||
c.createStatement(updateCommand)
|
||||
.bind(0, calData.host.name)
|
||||
.bind(1, calData.calendarId)
|
||||
.bind(2, calData.calendarAddress)
|
||||
.bind(3, calData.external)
|
||||
.bind(4, calData.credentialId)
|
||||
.bind(5, calData.privateKey)
|
||||
.bind(6, calData.encryptedAccessToken)
|
||||
.bind(7, calData.encryptedRefreshToken)
|
||||
.bind(8, calData.expiresAt.toEpochMilli())
|
||||
.bind(9, calData.guildId.asLong())
|
||||
.bind(10, calData.calendarNumber)
|
||||
.execute()
|
||||
).flatMapMany(Result::getRowsUpdated)
|
||||
.hasElements()
|
||||
.thenReturn(true)
|
||||
} else {
|
||||
val insertCommand = """INSERT INTO ${Tables.CALENDARS}
|
||||
(GUILD_ID, CALENDAR_NUMBER, HOST, CALENDAR_ID,
|
||||
CALENDAR_ADDRESS, EXTERNAL, CREDENTIAL_ID,
|
||||
PRIVATE_KEY, ACCESS_TOKEN, REFRESH_TOKEN, EXPIRES_AT) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""".trimMargin()
|
||||
|
||||
Mono.from(
|
||||
c.createStatement(insertCommand)
|
||||
.bind(0, calData.guildId.asLong())
|
||||
.bind(1, calData.calendarNumber)
|
||||
.bind(2, calData.host.name)
|
||||
.bind(3, calData.calendarId)
|
||||
.bind(4, calData.calendarAddress)
|
||||
.bind(5, calData.external)
|
||||
.bind(6, calData.credentialId)
|
||||
.bind(7, calData.privateKey)
|
||||
.bind(8, calData.encryptedAccessToken)
|
||||
.bind(9, calData.encryptedRefreshToken)
|
||||
.bind(10, calData.expiresAt.toEpochMilli())
|
||||
.execute()
|
||||
).flatMapMany(Result::getRowsUpdated)
|
||||
.hasElements()
|
||||
.thenReturn(true)
|
||||
}
|
||||
}.doOnError {
|
||||
LOGGER.error(DEFAULT, "Failed to update calendar data", it)
|
||||
}.onErrorResume { Mono.just(false) }
|
||||
}
|
||||
}
|
||||
|
||||
fun updateEventData(data: EventData): Mono<Boolean> {
|
||||
val id = if (data.eventId.contains("_"))
|
||||
data.eventId.split("_")[0]
|
||||
else
|
||||
data.eventId
|
||||
|
||||
return connect { c ->
|
||||
Mono.from(
|
||||
c.createStatement(Queries.SELECT_EVENT_BY_GUILD)
|
||||
.bind(0, data.guildId.asLong())
|
||||
.bind(1, id)
|
||||
.execute()
|
||||
).flatMapMany { res ->
|
||||
res.map { row, _ -> row }
|
||||
}.hasElements().flatMap { exists ->
|
||||
if (exists) {
|
||||
val updateCommand = """UPDATE ${Tables.EVENTS} SET
|
||||
CALENDAR_NUMBER = ?, IMAGE_LINK = ?, EVENT_END = ?
|
||||
WHERE EVENT_ID = ? AND GUILD_ID = ?
|
||||
""".trimMargin()
|
||||
|
||||
Mono.from(
|
||||
c.createStatement(updateCommand)
|
||||
.bind(0, data.calendarNumber)
|
||||
.bind(1, data.imageLink)
|
||||
.bind(2, data.eventEnd)
|
||||
.bind(3, id)
|
||||
.bind(4, data.guildId.asLong())
|
||||
.execute()
|
||||
).flatMapMany(Result::getRowsUpdated)
|
||||
.hasElements()
|
||||
.thenReturn(true)
|
||||
} else if (data.shouldBeSaved()) {
|
||||
val insertCommand = """INSERT INTO ${Tables.EVENTS}
|
||||
(GUILD_ID, EVENT_ID, CALENDAR_NUMBER, EVENT_END, IMAGE_LINK)
|
||||
VALUES(?, ?, ?, ?, ?)
|
||||
""".trimMargin()
|
||||
|
||||
Mono.from(
|
||||
c.createStatement(insertCommand)
|
||||
.bind(0, data.guildId.asLong())
|
||||
.bind(1, id)
|
||||
.bind(2, data.calendarNumber)
|
||||
.bind(3, data.eventEnd)
|
||||
.bind(4, data.imageLink)
|
||||
.execute()
|
||||
).flatMapMany(Result::getRowsUpdated)
|
||||
.hasElements()
|
||||
.thenReturn(true)
|
||||
} else {
|
||||
Mono.just(false)
|
||||
}.doOnError {
|
||||
LOGGER.error(DEFAULT, "Failed to update event data", it)
|
||||
}.onErrorResume { Mono.just(false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getAPIAccount(APIKey: String): Mono<UserAPIAccount> {
|
||||
return connect { c ->
|
||||
Mono.from(
|
||||
c.createStatement(Queries.SELECT_API_KEY)
|
||||
.bind(0, APIKey)
|
||||
.execute()
|
||||
).flatMapMany { res ->
|
||||
res.map { row, _ ->
|
||||
UserAPIAccount(
|
||||
row["USER_ID", String::class.java]!!,
|
||||
APIKey,
|
||||
row["BLOCKED", Boolean::class.java]!!,
|
||||
row["TIME_ISSUED", Long::class.java]!!
|
||||
)
|
||||
}
|
||||
}.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 API-key data", it)
|
||||
}.onErrorResume { Mono.empty() }
|
||||
}
|
||||
}
|
||||
|
||||
fun getSettings(guildId: Snowflake): Mono<GuildSettings> {
|
||||
if (DiscalCache.guildSettings.containsKey(guildId))
|
||||
return Mono.just(DiscalCache.guildSettings[guildId]!!)
|
||||
|
||||
return connect { c ->
|
||||
Mono.from(
|
||||
c.createStatement(Queries.SELECT_GUILD_SETTINGS)
|
||||
.bind(0, guildId.asLong())
|
||||
.execute()
|
||||
).flatMapMany { res ->
|
||||
res.map { row, _ ->
|
||||
val controlRole = row["CONTROL_ROLE", String::class.java]!!
|
||||
val announcementStyle = AnnouncementStyle.fromValue(row["ANNOUNCEMENT_STYLE", Int::class.java]!!)
|
||||
val timeFormat = TimeFormat.fromValue(row["TIME_FORMAT", Int::class.java]!!)
|
||||
val lang = row["LANG", String::class.java]!!
|
||||
val prefix = row["PREFIX", String::class.java]!!
|
||||
val patron = row["PATRON_GUILD", Boolean::class.java]!!
|
||||
val dev = row["DEV_GUILD", Boolean::class.java]!!
|
||||
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, eventKeepDuration,
|
||||
)
|
||||
|
||||
settings.dmAnnouncements.setFromString(dmAnnouncementsString)
|
||||
|
||||
//Store in cache...
|
||||
DiscalCache.guildSettings[guildId] = settings
|
||||
|
||||
settings
|
||||
}
|
||||
}.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 guild settings", it)
|
||||
}.onErrorReturn(GuildSettings.empty(guildId)).defaultIfEmpty(GuildSettings.empty(guildId))
|
||||
}
|
||||
}
|
||||
|
||||
fun getMainCalendar(guildId: Snowflake): Mono<CalendarData> = getCalendar(guildId, 1)
|
||||
|
||||
fun getCalendar(guildId: Snowflake, calendarNumber: Int): Mono<CalendarData> {
|
||||
return connect { c ->
|
||||
Mono.from(
|
||||
c.createStatement(Queries.SELECT_CALENDAR_BY_GUILD)
|
||||
.bind(0, guildId.asLong())
|
||||
.bind(1, calendarNumber)
|
||||
.execute()
|
||||
).flatMapMany { res ->
|
||||
res.map { row, _ ->
|
||||
val calId = row["CALENDAR_ID", String::class.java]!!
|
||||
val calNumber = row["CALENDAR_NUMBER", Int::class.java]!!
|
||||
val calAddr = row["CALENDAR_ADDRESS", String::class.java]!!
|
||||
val host = CalendarHost.valueOf(row["HOST", String::class.java]!!)
|
||||
val external = row["EXTERNAL", Boolean::class.java]!!
|
||||
val credId = row["CREDENTIAL_ID", Int::class.java]!!
|
||||
val privateKey = row["PRIVATE_KEY", String::class.java]!!
|
||||
val accessToken = row["ACCESS_TOKEN", String::class.java]!!
|
||||
val refreshToken = row["REFRESH_TOKEN", String::class.java]!!
|
||||
val expiresAt = Instant.ofEpochMilli(row["EXPIRES_AT", Long::class.java]!!)
|
||||
|
||||
CalendarData(
|
||||
guildId, calNumber, host, calId, calAddr, external,
|
||||
credId, privateKey, accessToken, refreshToken, expiresAt
|
||||
)
|
||||
}
|
||||
}.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 all guild calendars", it)
|
||||
}.onErrorResume { Mono.empty() }
|
||||
}
|
||||
}
|
||||
|
||||
fun getAllCalendars(guildId: Snowflake): Mono<List<CalendarData>> {
|
||||
return connect { c ->
|
||||
Mono.from(
|
||||
c.createStatement(Queries.SELECT_ALL_CALENDARS_BY_GUILD)
|
||||
.bind(0, guildId.asLong())
|
||||
.execute()
|
||||
).flatMapMany { res ->
|
||||
res.map { row, _ ->
|
||||
val calId = row["CALENDAR_ID", String::class.java]!!
|
||||
val calNumber = row["CALENDAR_NUMBER", Int::class.java]!!
|
||||
val calAddr = row["CALENDAR_ADDRESS", String::class.java]!!
|
||||
val host = CalendarHost.valueOf(row["HOST", String::class.java]!!)
|
||||
val external = row["EXTERNAL", Boolean::class.java]!!
|
||||
val credId = row["CREDENTIAL_ID", Int::class.java]!!
|
||||
val privateKey = row["PRIVATE_KEY", String::class.java]!!
|
||||
val accessToken = row["ACCESS_TOKEN", String::class.java]!!
|
||||
val refreshToken = row["REFRESH_TOKEN", String::class.java]!!
|
||||
val expiresAt = Instant.ofEpochMilli(row["EXPIRES_AT", Long::class.java]!!)
|
||||
|
||||
CalendarData(
|
||||
guildId, calNumber, host, calId, calAddr, external,
|
||||
credId, privateKey, accessToken, refreshToken, expiresAt
|
||||
)
|
||||
}
|
||||
}.collectList().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 all guild calendars", it)
|
||||
}.onErrorReturn(mutableListOf())
|
||||
}
|
||||
}
|
||||
|
||||
fun getCalendarCount(guildId: Snowflake): Mono<Int> {
|
||||
return connect { c ->
|
||||
Mono.from(
|
||||
c.createStatement(Queries.SELECT_CALENDAR_COUNT_BY_GUILD)
|
||||
.bind(0, guildId.asLong())
|
||||
.execute()
|
||||
).flatMapMany { res ->
|
||||
res.map { row, _ ->
|
||||
val calendars = row.get(0, Long::class.java)!!
|
||||
return@map calendars.toInt()
|
||||
}
|
||||
}.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 calendar count", it)
|
||||
}.onErrorReturn(-1)
|
||||
}.defaultIfEmpty(0)
|
||||
}
|
||||
|
||||
fun getEventData(guildId: Snowflake, eventId: String): Mono<EventData> {
|
||||
var eventIdLookup = eventId
|
||||
if (eventId.contains("_"))
|
||||
eventIdLookup = eventId.split("_")[0]
|
||||
|
||||
return connect { c ->
|
||||
Mono.from(
|
||||
c.createStatement(Queries.SELECT_EVENT_BY_GUILD)
|
||||
.bind(0, guildId.asLong())
|
||||
.bind(1, eventIdLookup)
|
||||
.execute()
|
||||
).flatMapMany { res ->
|
||||
res.map { row, _ ->
|
||||
|
||||
val id = row["EVENT_ID", String::class.java]!!
|
||||
val calNum = row["CALENDAR_NUMBER", Int::class.java]!!
|
||||
val end = row["EVENT_END", Long::class.java]!!
|
||||
val img = row["IMAGE_LINK", String::class.java]!!
|
||||
|
||||
EventData(guildId, id, calNum, end, img)
|
||||
}
|
||||
}.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 event data", it)
|
||||
}.onErrorResume {
|
||||
Mono.empty()
|
||||
}
|
||||
}.defaultIfEmpty(EventData(guildId, eventId = eventIdLookup))
|
||||
}
|
||||
|
||||
fun deleteAnnouncementsForEvent(guildId: Snowflake, eventId: String): Mono<Boolean> {
|
||||
return connect { c ->
|
||||
Mono.from(
|
||||
c.createStatement(Queries.DELETE_ANNOUNCEMENTS_FOR_EVENT)
|
||||
.bind(0, eventId)
|
||||
.bind(1, guildId.asLong())
|
||||
.execute()
|
||||
).flatMapMany(Result::getRowsUpdated)
|
||||
.hasElements()
|
||||
.thenReturn(true)
|
||||
.doOnError {
|
||||
LOGGER.error(DEFAULT, "Failed to delete announcements for event", it)
|
||||
}.onErrorReturn(false)
|
||||
}.defaultIfEmpty(false)
|
||||
}
|
||||
|
||||
fun deleteEventData(eventId: String): Mono<Boolean> {
|
||||
if (eventId.contains("_")) return Mono.empty() // Don't delete if child event of recurring parent.
|
||||
return connect { c ->
|
||||
Mono.from(
|
||||
c.createStatement(Queries.DELETE_EVENT_DATA)
|
||||
.bind(0, eventId)
|
||||
.execute()
|
||||
).flatMapMany(Result::getRowsUpdated)
|
||||
.hasElements()
|
||||
.thenReturn(true)
|
||||
.doOnError {
|
||||
LOGGER.error(DEFAULT, "Failed to delete event data", it)
|
||||
}.onErrorReturn(false)
|
||||
}.defaultIfEmpty(false)
|
||||
}
|
||||
|
||||
/* Utility Deletion Methods */
|
||||
|
||||
fun deleteCalendarAndRelatedData(calendarData: CalendarData): Mono<Boolean> {
|
||||
return connect { c ->
|
||||
Mono.from(
|
||||
c.createStatement(Queries.FULL_CALENDAR_DELETE) //Monolith 8 statement query
|
||||
// calendar delete bindings
|
||||
.bind(0, calendarData.guildId.asLong())
|
||||
.bind(1, calendarData.calendarNumber)
|
||||
// event delete bindings
|
||||
.bind(2, calendarData.guildId.asLong())
|
||||
.bind(3, calendarData.calendarNumber)
|
||||
// rsvp delete bindings
|
||||
.bind(4, calendarData.guildId.asLong())
|
||||
.bind(5, calendarData.calendarNumber)
|
||||
// announcement delete bindings
|
||||
.bind(6, calendarData.guildId.asLong())
|
||||
.bind(7, calendarData.calendarNumber)
|
||||
// delete static message bindings
|
||||
.bind(8, calendarData.guildId.asLong())
|
||||
.bind(9, calendarData.calendarNumber)
|
||||
// decrement calendar bindings
|
||||
.bind(10, calendarData.calendarNumber)
|
||||
.bind(11, calendarData.guildId.asLong())
|
||||
// decrement event bindings
|
||||
.bind(12, calendarData.calendarNumber)
|
||||
.bind(13, calendarData.guildId.asLong())
|
||||
// decrement rsvp bindings
|
||||
.bind(14, calendarData.calendarNumber)
|
||||
.bind(15, calendarData.guildId.asLong())
|
||||
// decrement announcement bindings
|
||||
.bind(16, calendarData.calendarNumber)
|
||||
.bind(17, calendarData.guildId.asLong())
|
||||
// decrement static message bindings
|
||||
.bind(18, calendarData.calendarNumber)
|
||||
.bind(19, calendarData.guildId.asLong())
|
||||
.execute()
|
||||
).flatMapMany(Result::getRowsUpdated)
|
||||
.hasElements()
|
||||
.thenReturn(true)
|
||||
.doOnError {
|
||||
LOGGER.error(DEFAULT, "Full calendar delete failed!", it)
|
||||
}.onErrorReturn(false)
|
||||
}.defaultIfEmpty(true) // If nothing was updated and no error was emitted, it's safe to return this worked.
|
||||
}
|
||||
|
||||
fun deleteAllDataForGuild(guildId: Snowflake): Mono<Boolean> {
|
||||
return connect { c ->
|
||||
Mono.from(
|
||||
c.createStatement(Queries.DELETE_EVERYTHING_FOR_GUILD) //Monolith 6 statement query
|
||||
// settings delete bindings
|
||||
.bind(0, guildId.asLong())
|
||||
// calendar delete bindings
|
||||
.bind(1, guildId.asLong())
|
||||
// event delete bindings
|
||||
.bind(2, guildId.asLong())
|
||||
// rsvp delete bindings
|
||||
.bind(3, guildId.asLong())
|
||||
// announcement delete bindings
|
||||
.bind(4, guildId.asLong())
|
||||
// static message delete bindings
|
||||
.bind(5, guildId.asLong())
|
||||
.execute()
|
||||
).flatMapMany(Result::getRowsUpdated)
|
||||
.hasElements()
|
||||
.thenReturn(true)
|
||||
.doOnError {
|
||||
LOGGER.error(DEFAULT, "Full data delete failed!", it)
|
||||
}.onErrorReturn(false)
|
||||
}.defaultIfEmpty(true) // If nothing was updated and no error was emitted, it's safe to return this worked.
|
||||
}
|
||||
|
||||
/* Event Data */
|
||||
|
||||
fun getEventsData(guildId: Snowflake, eventIds: List<String>): Mono<Map<String, EventData>> {
|
||||
// clean up IDs
|
||||
val idsToUse = mutableListOf<String>()
|
||||
eventIds.forEach {
|
||||
val id = if (it.contains("_")) it.split("_")[0] else it
|
||||
|
||||
if (!idsToUse.contains(id)) idsToUse.add(id)
|
||||
}
|
||||
|
||||
if (idsToUse.isEmpty()) return Mono.just(emptyMap())
|
||||
|
||||
// Convert our list of IDs to sql escaped string
|
||||
val builder = StringBuilder()
|
||||
idsToUse.withIndex().forEach {
|
||||
if (it.index != idsToUse.size - 1) {
|
||||
builder.append("'${it.value}', ")
|
||||
} else {
|
||||
builder.append("'${it.value}'")
|
||||
}
|
||||
}
|
||||
|
||||
return connect { c ->
|
||||
Mono.from(
|
||||
//Have to do it this way, sql injection is not possible as these IDs are not user input
|
||||
c.createStatement(Queries.SELECT_MANY_EVENT_DATA.replace("?", builder.toString()))
|
||||
.execute()
|
||||
).flatMapMany { res ->
|
||||
res.map { row, _ ->
|
||||
val id = row["EVENT_ID", String::class.java]!!
|
||||
val calNum = row["CALENDAR_NUMBER", Int::class.java]!!
|
||||
val end = row["EVENT_END", Long::class.java]!!
|
||||
val img = row["IMAGE_LINK", String::class.java]!!
|
||||
|
||||
EventData(guildId, id, calNum, end, img)
|
||||
}
|
||||
}.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 many event data", it)
|
||||
}.onErrorResume {
|
||||
Mono.empty()
|
||||
}.collectMap {
|
||||
it.eventId
|
||||
}.defaultIfEmpty(emptyMap())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object Queries {
|
||||
@Language("MySQL")
|
||||
val SELECT_API_KEY = """SELECT * FROM ${Tables.API}
|
||||
WHERE API_KEY = ?
|
||||
""".trimMargin()
|
||||
|
||||
@Language("MySQL")
|
||||
val SELECT_GUILD_SETTINGS = """SELECT * FROM ${Tables.GUILD_SETTINGS}
|
||||
WHERE GUILD_ID = ?
|
||||
""".trimMargin()
|
||||
|
||||
@Language("MySQL")
|
||||
val SELECT_CALENDAR_BY_GUILD = """SELECT * FROM ${Tables.CALENDARS}
|
||||
WHERE GUILD_ID = ? AND CALENDAR_NUMBER = ?
|
||||
""".trimMargin()
|
||||
|
||||
@Language("MySQL")
|
||||
val SELECT_ALL_CALENDARS_BY_GUILD = """SELECT * FROM ${Tables.CALENDARS}
|
||||
WHERE GUILD_ID = ?
|
||||
""".trimMargin()
|
||||
|
||||
@Language("MySQL")
|
||||
val SELECT_CALENDAR_COUNT_BY_GUILD = """SELECT COUNT(*) FROM ${Tables.CALENDARS}
|
||||
WHERE GUILD_ID = ?
|
||||
""".trimMargin()
|
||||
|
||||
@Language("MySQL")
|
||||
val SELECT_EVENT_BY_GUILD = """SELECT * FROM ${Tables.EVENTS}
|
||||
WHERE GUILD_ID = ? AND EVENT_ID = ?
|
||||
""".trimMargin()
|
||||
|
||||
@Language("MySQL")
|
||||
val DELETE_ANNOUNCEMENTS_FOR_EVENT = """DELETE FROM ${Tables.ANNOUNCEMENTS}
|
||||
WHERE EVENT_ID = ? AND GUILD_ID = ?
|
||||
""".trimMargin()
|
||||
|
||||
@Language("MySQL")
|
||||
val DELETE_EVENT_DATA = """DELETE FROM ${Tables.EVENTS}
|
||||
WHERE EVENT_ID = ?
|
||||
""".trimMargin()
|
||||
|
||||
@Language("MySQL")
|
||||
val DELETE_ALL_EVENT_DATA = """DELETE FROM ${Tables.EVENTS}
|
||||
WHERE GUILD_ID = ? AND CALENDAR_NUMBER = ?
|
||||
""".trimMargin()
|
||||
|
||||
@Language("MySQL")
|
||||
val DELETE_ALL_ANNOUNCEMENT_DATA = """DELETE FROM ${Tables.ANNOUNCEMENTS}
|
||||
WHERE GUILD_ID = ? AND CALENDAR_NUMBER = ?
|
||||
""".trimMargin()
|
||||
|
||||
@Language("MySQL")
|
||||
val DELETE_ALL_RSVP_DATA = """DELETE FROM ${Tables.RSVP}
|
||||
WHERE GUILD_ID = ? AND CALENDAR_NUMBER = ?
|
||||
""".trimMargin()
|
||||
|
||||
@Language("MySQL")
|
||||
val DELETE_CALENDAR = """DELETE FROM ${Tables.CALENDARS}
|
||||
WHERE GUILD_ID = ? AND calendar_number = ?
|
||||
""".trimMargin()
|
||||
|
||||
@Language("MySQL")
|
||||
val DECREMENT_CALENDARS = """UPDATE ${Tables.CALENDARS}
|
||||
SET calendar_number = calendar_number - 1
|
||||
WHERE calendar_number >=? AND guild_id = ?
|
||||
""".trimMargin()
|
||||
|
||||
@Language("MySQL")
|
||||
val DECREMENT_ANNOUNCEMENTS = """UPDATE ${Tables.ANNOUNCEMENTS}
|
||||
SET calendar_number = calendar_number - 1
|
||||
WHERE calendar_number >=? AND guild_id = ?
|
||||
""".trimMargin()
|
||||
|
||||
@Language("MySQL")
|
||||
val DECREMENT_EVENTS = """UPDATE ${Tables.EVENTS}
|
||||
SET calendar_number = calendar_number - 1
|
||||
WHERE calendar_number >=? AND guild_id = ?
|
||||
""".trimMargin()
|
||||
|
||||
@Language("MySQL")
|
||||
val DECREMENT_RSVPS = """UPDATE ${Tables.RSVP}
|
||||
SET calendar_number = calendar_number - 1
|
||||
WHERE calendar_number >=? AND guild_id = ?
|
||||
""".trimMargin()
|
||||
|
||||
@Language("MySQL")
|
||||
val DECREMENT_STATIC_MESSAGES = """UPDATE ${Tables.STATIC_MESSAGES}
|
||||
SET calendar_number = calendar_number - 1
|
||||
WHERE calendar_number >=? AND guild_id = ?
|
||||
""".trimMargin()
|
||||
|
||||
@Language("MySQL")
|
||||
val DELETE_ALL_STATIC_MESSAGES = """DELETE FROM ${Tables.STATIC_MESSAGES}
|
||||
WHERE guild_id = ? AND calendar_number = ?
|
||||
""".trimMargin()
|
||||
|
||||
@Language("MySQL")
|
||||
val FULL_CALENDAR_DELETE = """
|
||||
$DELETE_CALENDAR;$DELETE_ALL_EVENT_DATA;$DELETE_ALL_RSVP_DATA;$DELETE_ALL_ANNOUNCEMENT_DATA;$DELETE_ALL_STATIC_MESSAGES;
|
||||
$DECREMENT_CALENDARS;$DECREMENT_EVENTS;$DECREMENT_RSVPS;$DECREMENT_ANNOUNCEMENTS;$DECREMENT_STATIC_MESSAGES
|
||||
""".trimIndent()
|
||||
|
||||
|
||||
@Language("MySQL")
|
||||
val SELECT_MANY_EVENT_DATA = """SELECT * FROM ${Tables.EVENTS}
|
||||
WHERE event_id in (?)
|
||||
""".trimMargin()
|
||||
|
||||
/* Delete everything */
|
||||
|
||||
@Language("MySQL")
|
||||
val DELETE_EVERYTHING_FOR_GUILD = """
|
||||
delete from ${Tables.GUILD_SETTINGS} where GUILD_ID=?;
|
||||
delete from ${Tables.CALENDARS} where GUILD_ID=?;
|
||||
delete from ${Tables.EVENTS} where GUILD_ID=?;
|
||||
delete from ${Tables.RSVP} where GUILD_ID=?;
|
||||
delete from ${Tables.ANNOUNCEMENTS} where GUILD_ID=?;
|
||||
delete from ${Tables.STATIC_MESSAGES} where guild_id=?;
|
||||
""".trimMargin()
|
||||
}
|
||||
|
||||
private object Tables {
|
||||
/* The language annotations are there because IntelliJ is dumb and assumes this needs to be proper MySQL */
|
||||
|
||||
@Language("Kotlin")
|
||||
const val API: String = "api"
|
||||
|
||||
@Language("Kotlin")
|
||||
const val GUILD_SETTINGS = "guild_settings"
|
||||
|
||||
@Language("Kotlin")
|
||||
const val CALENDARS = "calendars"
|
||||
|
||||
@Language("Kotlin")
|
||||
const val ANNOUNCEMENTS = "announcements"
|
||||
|
||||
@Language("Kotlin")
|
||||
const val EVENTS = "events"
|
||||
|
||||
@Language("Kotlin")
|
||||
const val RSVP = "rsvp"
|
||||
|
||||
@Language("Kotlin")
|
||||
const val STATIC_MESSAGES = "static_messages"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.dreamexposure.discal.core.database
|
||||
|
||||
import org.springframework.data.relational.core.mapping.Table
|
||||
|
||||
@Table("events")
|
||||
data class EventMetadataData(
|
||||
val guildId: Long,
|
||||
val eventId: String,
|
||||
val calendarNumber: Int,
|
||||
val eventEnd: Long,
|
||||
val imageLink: String,
|
||||
)
|
||||
@@ -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 EventMetadataRepository : R2dbcRepository<EventMetadataData, Long> {
|
||||
fun existsByGuildIdAndEventId(guildId: Long, eventId: String): Mono<Boolean>
|
||||
|
||||
fun findByGuildIdAndEventId(guildId: Long, eventId: String): Mono<EventMetadataData>
|
||||
|
||||
fun findAllByGuildIdAndEventIdIn(guildId: Long, eventIds: Collection<String>): Flux<EventMetadataData>
|
||||
|
||||
@Query("""
|
||||
UPDATE events
|
||||
SET calendar_number = :calendarNumber,
|
||||
event_end = :eventEnd,
|
||||
image_link = :imageLink
|
||||
WHERE guild_id = :guildId AND event_id = :eventId
|
||||
""")
|
||||
fun updateByGuildIdAndEventId(
|
||||
guildId: Long,
|
||||
eventId: String,
|
||||
calendarNumber: Int,
|
||||
eventEnd: Long,
|
||||
imageLink: String,
|
||||
): Mono<Long>
|
||||
|
||||
fun deleteByGuildIdAndEventId(guildId: Long, eventId: String): Mono<Void>
|
||||
|
||||
fun deleteAllByGuildIdAndCalendarNumber(guildId: Long, calendarNumber: Int): Mono<Void>
|
||||
|
||||
@Query("""
|
||||
UPDATE events
|
||||
SET calendar_number = calendar_number - 1
|
||||
WHERE calendar_number >= :calendarNumber AND guild_id = :guildId
|
||||
""")
|
||||
fun decrementCalendarsByGuildIdAndCalendarNumber(guildId: Long, calendarNumber: Int): Mono<Long>
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.dreamexposure.discal.core.database
|
||||
|
||||
import org.springframework.data.relational.core.mapping.Table
|
||||
|
||||
@Table("guild_settings")
|
||||
data class GuildSettingsData(
|
||||
val guildId: Long,
|
||||
|
||||
val controlRole: String,
|
||||
val timeFormat: Int,
|
||||
val patronGuild: Boolean,
|
||||
val devGuild : Boolean,
|
||||
val maxCalendars: Int,
|
||||
val lang: String,
|
||||
val branded: Boolean,
|
||||
val announcementStyle: Int,
|
||||
val eventKeepDuration: Boolean,
|
||||
)
|
||||
@@ -0,0 +1,39 @@
|
||||
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 GuildSettingsRepository : R2dbcRepository<GuildSettingsData, Long> {
|
||||
|
||||
fun existsByGuildId(guildId: Long): Mono<Boolean>
|
||||
|
||||
fun findByGuildId(guildId: Long): Mono<GuildSettingsData>
|
||||
|
||||
@Query("""
|
||||
UPDATE guild_settings
|
||||
SET control_role = :controlRole,
|
||||
time_format = :timeFormat,
|
||||
patron_guild = :patronGuild,
|
||||
dev_guild = :devGuild,
|
||||
max_calendars = :maxCalendars,
|
||||
lang = :lang,
|
||||
branded = :branded,
|
||||
announcement_style = :announcementStyle,
|
||||
event_keep_duration = :eventKeepDuration
|
||||
WHERE guild_id = :guildId
|
||||
""")
|
||||
fun updateByGuildId(
|
||||
guildId: Long,
|
||||
|
||||
controlRole: String,
|
||||
timeFormat: Int,
|
||||
patronGuild: Boolean,
|
||||
devGuild: Boolean,
|
||||
maxCalendars: Int,
|
||||
lang: String,
|
||||
branded: Boolean,
|
||||
announcementStyle: Int,
|
||||
eventKeepDuration: Boolean,
|
||||
): Mono<Int>
|
||||
}
|
||||
@@ -43,4 +43,13 @@ interface RsvpRepository: R2dbcRepository<RsvpData, String> {
|
||||
WHERE guild_id = :guildId AND rsvp_role = :rsvpRole
|
||||
""")
|
||||
fun removeRoleByGuildIdAndRsvpRole(guildId: Long, rsvpRole: Long): Mono<Int>
|
||||
|
||||
fun deleteAllByGuildIdAndCalendarNumber(guildId: Long, calendarNumber: Int): Mono<Void>
|
||||
|
||||
@Query("""
|
||||
UPDATE rsvp
|
||||
SET calendar_number = calendar_number - 1
|
||||
WHERE calendar_number >= :calendarNumber AND guild_id = :guildId
|
||||
""")
|
||||
fun decrementCalendarsByGuildIdAndCalendarNumber(guildId: Long, calendarNumber: Int): Mono<Long>
|
||||
}
|
||||
|
||||
@@ -45,5 +45,14 @@ interface StaticMessageRepository: R2dbcRepository<StaticMessageData, Long> {
|
||||
calendarNumber: Int,
|
||||
): Mono<Int>
|
||||
|
||||
fun deleteByGuildIdAndMessageId(guildId: Long, messageId: Long): Mono<Void>
|
||||
fun deleteAllByGuildIdAndMessageId(guildId: Long, messageId: Long): Mono<Void>
|
||||
|
||||
fun deleteByGuildIdAndCalendarNumber(guildId: Long, calendarNumber: Int): Mono<Void>
|
||||
|
||||
@Query("""
|
||||
UPDATE static_messages
|
||||
SET calendar_number = calendar_number - 1
|
||||
WHERE calendar_number >= :calendarNumber AND guild_id = :guildId
|
||||
""")
|
||||
fun decrementCalendarsByGuildIdAndCalendarNumber(guildId: Long, calendarNumber: Int): Mono<Long>
|
||||
}
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
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`.calendar.CalendarData
|
||||
import org.dreamexposure.discal.core.`object`.web.WebCalendar
|
||||
import org.json.JSONObject
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
interface Calendar {
|
||||
/**
|
||||
* The ID of the [Guild] this calendar belongs to.
|
||||
*/
|
||||
val guildId: Snowflake
|
||||
get() = calendarData.guildId
|
||||
|
||||
/**
|
||||
* The base object in which most of the calendar data comes from
|
||||
*/
|
||||
val calendarData: CalendarData
|
||||
|
||||
/**
|
||||
* The calendar's ID, usually but not always the same as the calendar's [address][calendarAddress].
|
||||
* Use this for unique identification of the calendar
|
||||
*/
|
||||
val calendarId: String
|
||||
get() = calendarData.calendarId
|
||||
|
||||
/**
|
||||
* The calendar's address, usually but not always the same as the calendar's [ID][calendarId].
|
||||
* This property may not be unique, use [calendarId] instead
|
||||
*/
|
||||
val calendarAddress: String
|
||||
get() = calendarData.calendarAddress
|
||||
|
||||
/**
|
||||
* The relative number of the calendar in order it was created in for the [Guild].
|
||||
* Calendar number `1` is the `main` calendar for the [Guild], used as the default.
|
||||
*/
|
||||
val calendarNumber: Int
|
||||
get() = calendarData.calendarNumber
|
||||
|
||||
/**
|
||||
* Whether the calendar is "external" meaning it is owned by a user account.
|
||||
* This does not indicate the service used to host the calendar, but whether it is owned by DisCal, or a user.
|
||||
*/
|
||||
val external: Boolean
|
||||
get() = calendarData.external
|
||||
|
||||
/**
|
||||
* The name of the calendar. Renamed from "summary" to be more user-friendly and clear.
|
||||
*/
|
||||
val name: String
|
||||
|
||||
/**
|
||||
* A longer form description of the calendar.
|
||||
* If this is not present, an empty string is returned
|
||||
*/
|
||||
val description: String
|
||||
|
||||
/**
|
||||
* The timezone the calendar uses. Normally in its longer name, such as `America/New_York`
|
||||
*/
|
||||
val timezone: ZoneId
|
||||
|
||||
/**
|
||||
* The timezone's name, derived from the ZoneId timezone
|
||||
*/
|
||||
val zoneName: String
|
||||
get() = timezone.id
|
||||
|
||||
/**
|
||||
* A link to view the calendar on the official discal website
|
||||
*/
|
||||
val link: String
|
||||
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)
|
||||
*/
|
||||
val hostLink: String
|
||||
|
||||
//Reactive - Self
|
||||
/**
|
||||
* Attempts to delete the calendar and returns the result.
|
||||
* If an error occurs, it is emitted through the [Mono].
|
||||
*
|
||||
* @return A [Mono] boolean telling whether the deletion was successful
|
||||
*/
|
||||
fun delete(): Mono<Boolean>
|
||||
|
||||
/**
|
||||
* Attempts to update the calendar with the provided details and return the result.
|
||||
* If an error occurs, it is emitted through the [Mono].
|
||||
*
|
||||
* @param spec The details to update the calendar with
|
||||
* @return A [Mono] whereupon successful completion, returns an [UpdateCalendarResponse] with the new calendar
|
||||
*/
|
||||
fun update(spec: UpdateCalendarSpec): Mono<UpdateCalendarResponse>
|
||||
|
||||
//Reactive - Events
|
||||
/**
|
||||
* Requests to retrieve the [Event] with the ID.
|
||||
* If an error occurs, it is emitted through the [Mono]
|
||||
*
|
||||
* @return A [Mono] of the [Event], or [Empty][Mono.empty] if not found
|
||||
*/
|
||||
fun getEvent(eventId: String): Mono<Event>
|
||||
|
||||
/**
|
||||
* Requests to retrieve all upcoming [events][Event]
|
||||
* If an error occurs, it is emitted through the [Flux]
|
||||
*
|
||||
* @param amount The upper limit of how many events to retrieve
|
||||
* @return A [Flux] of [events][Event] that are upcoming
|
||||
*/
|
||||
fun getUpcomingEvents(amount: Int): Flux<Event>
|
||||
|
||||
/**
|
||||
* Requests to retrieve all ongoing [events][Event] (starting no more than 2 weeks ago).
|
||||
* If an error occurs, it is emitted through the [Flux]
|
||||
*
|
||||
* @return A [Flux] of [events][Event] that are currently ongoing
|
||||
*/
|
||||
fun getOngoingEvents(): Flux<Event>
|
||||
|
||||
/**
|
||||
* Requests to retrieve all [events][Event] within the supplied time span (inclusive).
|
||||
* If an error occurs, it is emitted through the [Flux]
|
||||
*
|
||||
* @return A [Flux] of [events][Event] that are happening within the supplied time range
|
||||
*/
|
||||
fun getEventsInTimeRange(start: Instant, end: Instant): Flux<Event>
|
||||
|
||||
/**
|
||||
* Requests to retrieve all [events][Event] occurring withing the next 24-hour period from the supplied [Instant]
|
||||
* (inclusive).
|
||||
* If an error occurs, it is emitted through the [Flux]
|
||||
*
|
||||
* @return A [Flux] of [events][Event] that are happening within the next 24-hour period from the start.
|
||||
*/
|
||||
fun getEventsInNext24HourPeriod(start: Instant): Flux<Event> =
|
||||
getEventsInTimeRange(start, start.plus(1, ChronoUnit.DAYS))
|
||||
|
||||
/**
|
||||
* Requests to retrieve all [events][Event] within the month starting at the supplied [Instant].
|
||||
* If an error occurs, it is emitted through the [Flux]
|
||||
*
|
||||
* @return A [Flux] of [events][Event] that are happening in the supplied 1-month period.
|
||||
*/
|
||||
fun getEventsInMonth(start: Instant, daysInMonth: Int): Flux<Event> =
|
||||
getEventsInTimeRange(start, start.plus(daysInMonth.toLong(), ChronoUnit.DAYS))
|
||||
|
||||
fun getEventsInNextNDays(days: Int): Flux<Event> =
|
||||
getEventsInTimeRange(Instant.now(), Instant.now().plus(days.toLong(), ChronoUnit.DAYS))
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Requests to create an event with the supplied information.
|
||||
* If an error occurs, it is emitted through the [Mono]
|
||||
*
|
||||
* @param spec The information to input into the new [Event]
|
||||
* @return A [Mono] containing the newly created [Event]
|
||||
*/
|
||||
fun createEvent(spec: CreateEventSpec): Mono<Event>
|
||||
|
||||
//Convenience
|
||||
|
||||
/**
|
||||
* Converts this entity into a [WebCalendar] object.
|
||||
*
|
||||
* @return A [WebCalendar] containing the information from this entity
|
||||
*/
|
||||
fun toWebCalendar(): WebCalendar {
|
||||
return WebCalendar(
|
||||
this.calendarId,
|
||||
this.calendarAddress,
|
||||
this.calendarNumber,
|
||||
this.calendarData.host,
|
||||
this.link,
|
||||
this.hostLink,
|
||||
this.name,
|
||||
this.description,
|
||||
this.timezone.id.replace("/", "___"),
|
||||
this.external
|
||||
)
|
||||
}
|
||||
|
||||
fun toJson(): JSONObject {
|
||||
return JSONObject()
|
||||
.put("guild_id", guildId.asString())
|
||||
.put("calendar_id", calendarId)
|
||||
.put("calendar_address", calendarAddress)
|
||||
.put("calendar_number", calendarNumber)
|
||||
.put("host", calendarData.host.name)
|
||||
.put("host_link", hostLink)
|
||||
.put("external", external)
|
||||
.put("name", name)
|
||||
.put("description", description)
|
||||
.put("timezone", timezone)
|
||||
.put("link", link)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Requests to retrieve the [Calendar] from the provided [CalendarData]
|
||||
* If an error occurs, it is emitted through the [Mono]
|
||||
*
|
||||
* @param data The data object for the Calendar to be built with
|
||||
* @return A [Mono] containing the [Calendar], if it does not exist, [empty][Mono.empty] is returned.
|
||||
*/
|
||||
fun from(data: CalendarData): Mono<Calendar> {
|
||||
when (data.host) {
|
||||
CalendarHost.GOOGLE -> {
|
||||
return GoogleCalendar.get(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
package org.dreamexposure.discal.core.entities
|
||||
|
||||
import discord4j.common.util.Snowflake
|
||||
import discord4j.core.`object`.entity.Guild
|
||||
import kotlinx.serialization.encodeToString
|
||||
import org.dreamexposure.discal.core.entities.response.UpdateEventResponse
|
||||
import org.dreamexposure.discal.core.entities.spec.update.UpdateEventSpec
|
||||
import org.dreamexposure.discal.core.enums.event.EventColor
|
||||
import org.dreamexposure.discal.core.`object`.event.EventData
|
||||
import org.dreamexposure.discal.core.`object`.event.Recurrence
|
||||
import org.dreamexposure.discal.core.utils.GlobalVal.JSON_FORMAT
|
||||
import org.json.JSONObject
|
||||
import reactor.core.publisher.Mono
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
interface Event {
|
||||
/**
|
||||
* The ID of the event.
|
||||
* In the format eXXXXXXXXXX if generated by DisCal, otherwise it was generated by the 3rd party calendar service
|
||||
*/
|
||||
val eventId: String
|
||||
|
||||
/**
|
||||
* The ID of the [Guild] this event belongs to.
|
||||
*/
|
||||
val guildId: Snowflake
|
||||
get() = calendar.guildId
|
||||
|
||||
/**
|
||||
* The [Calendar] this Event belongs to
|
||||
*/
|
||||
val calendar: Calendar
|
||||
|
||||
/**
|
||||
* The of the event saved to the DisCal database
|
||||
*/
|
||||
val eventData: EventData
|
||||
|
||||
/**
|
||||
* The name of the event, renamed from "summary" to make it more user-friendly and clear.
|
||||
*/
|
||||
val name: String
|
||||
|
||||
/**
|
||||
* A description of what the event is about.
|
||||
*/
|
||||
val description: String
|
||||
|
||||
/**
|
||||
* The location at which the event occurs, usually a map location.
|
||||
*/
|
||||
val location: String
|
||||
|
||||
/**
|
||||
* The link to view the event at
|
||||
*/
|
||||
val link: String
|
||||
|
||||
/**
|
||||
* The color of the event. Used for visually identifying it in Discord embeds.
|
||||
* If no event color is assigned, it returns [EventColor.NONE] which is DisCal blue.
|
||||
*/
|
||||
val color: EventColor
|
||||
|
||||
/**
|
||||
* The start of the event, as an [Instant] representing the time starting from January 1st 1970.
|
||||
*/
|
||||
val start: Instant
|
||||
|
||||
/**
|
||||
* The end of the event, as an [Instant] representing the time starting from January 1st 1970.
|
||||
*/
|
||||
val end: Instant
|
||||
|
||||
/**
|
||||
* Whether the event is a recurring event.
|
||||
*/
|
||||
val recur: Boolean
|
||||
|
||||
/**
|
||||
* The rules of the recurring event. Contains the RRule an human-readable information on how the event will recur
|
||||
*/
|
||||
val recurrence: Recurrence
|
||||
|
||||
/**
|
||||
* A link to the image, if none is present, returns empty
|
||||
*/
|
||||
val image: String
|
||||
get() = eventData.imageLink
|
||||
|
||||
/**
|
||||
* The timezone that the event takes place in. This is always the same as the [Calendar]'s timezone
|
||||
*/
|
||||
val timezone: ZoneId
|
||||
get() = calendar.timezone
|
||||
|
||||
//Reactive
|
||||
|
||||
|
||||
/**
|
||||
* Attempts to update the event and returns the result.
|
||||
* If an error occurs, it is emitted through the [Mono].
|
||||
*
|
||||
* @param spec The information to update the event with
|
||||
* @return A [Mono] that contains the [UpdateEventResponse] containing information on success and the changes.
|
||||
*/
|
||||
fun update(spec: UpdateEventSpec): Mono<UpdateEventResponse>
|
||||
|
||||
/**
|
||||
* Attempts to delete the event and returns the result.
|
||||
* If an error occurs, it is emitted through the [Mono].
|
||||
*
|
||||
* @return A [Mono] containing whether delete succeeded.
|
||||
*/
|
||||
fun delete(): Mono<Boolean>
|
||||
|
||||
fun isOngoing(): Boolean = start.isBefore(Instant.now()) && end.isAfter(Instant.now())
|
||||
|
||||
fun isOver(): Boolean = end.isBefore(Instant.now())
|
||||
|
||||
fun isStarted() = start.isBefore(Instant.now())
|
||||
|
||||
/**
|
||||
* Whether the event is 24 hours long.
|
||||
*
|
||||
* @return Whether the event is 24 hours long
|
||||
*/
|
||||
fun is24Hours() = Duration.between(start, end).toHours() == 24L
|
||||
|
||||
/**
|
||||
* Whether the event lasts for a full calendar day (midnight to midnight) or longer.
|
||||
*
|
||||
* @return Whether the event is all day
|
||||
*/
|
||||
fun isAllDay(): Boolean {
|
||||
val start = this.start.atZone(timezone)
|
||||
|
||||
return start.hour == 0 && is24Hours()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the event spans across multiple calendar days (ex Monday 8pm to Tuesday 3am)
|
||||
*
|
||||
* @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)
|
||||
|
||||
return when {
|
||||
start.year != end.year -> true
|
||||
start.month != end.month -> true
|
||||
start.dayOfYear != end.dayOfYear -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
//Json bullshit
|
||||
fun toJson(): JSONObject {
|
||||
return JSONObject()
|
||||
.put("guild_id", guildId)
|
||||
.put("calendar", calendar.toJson())
|
||||
.put("event_id", eventId)
|
||||
.put("epoch_start", start.toEpochMilli())
|
||||
.put("epoch_end", end.toEpochMilli())
|
||||
.put("name", name)
|
||||
.put("description", description)
|
||||
.put("location", location)
|
||||
.put("is_parent", !eventId.contains("_"))
|
||||
.put("color", color.name)
|
||||
.put("recur", recur)
|
||||
.put("recurrence", JSONObject(JSON_FORMAT.encodeToString(recurrence)))
|
||||
.put("rrule", recurrence.toRRule())
|
||||
.put("image", eventData.imageLink)
|
||||
}
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
package org.dreamexposure.discal.core.entities.google
|
||||
|
||||
import com.google.api.client.util.DateTime
|
||||
import com.google.api.services.calendar.model.AclRule
|
||||
import com.google.api.services.calendar.model.EventDateTime
|
||||
import org.dreamexposure.discal.core.`object`.calendar.CalendarData
|
||||
import org.dreamexposure.discal.core.`object`.event.EventData
|
||||
import org.dreamexposure.discal.core.cache.DiscalCache
|
||||
import org.dreamexposure.discal.core.crypto.KeyGenerator
|
||||
import org.dreamexposure.discal.core.database.DatabaseManager
|
||||
import org.dreamexposure.discal.core.entities.Calendar
|
||||
import org.dreamexposure.discal.core.entities.Event
|
||||
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.event.EventColor
|
||||
import org.dreamexposure.discal.core.extensions.google.asInstant
|
||||
import org.dreamexposure.discal.core.wrapper.google.AclRuleWrapper
|
||||
import org.dreamexposure.discal.core.wrapper.google.CalendarWrapper
|
||||
import org.dreamexposure.discal.core.wrapper.google.EventWrapper
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import com.google.api.services.calendar.model.Calendar as GoogleCalendarModel
|
||||
import com.google.api.services.calendar.model.Event as GoogleEventModel
|
||||
|
||||
class GoogleCalendar internal constructor(
|
||||
override val calendarData: CalendarData,
|
||||
private val baseCalendar: GoogleCalendarModel
|
||||
) : Calendar {
|
||||
|
||||
override val name: String
|
||||
get() = baseCalendar.summary.orEmpty()
|
||||
|
||||
override val description: String
|
||||
get() = baseCalendar.description.orEmpty()
|
||||
|
||||
override val timezone: ZoneId
|
||||
get() = ZoneId.of(baseCalendar.timeZone)
|
||||
|
||||
override val hostLink: String
|
||||
get() = "https://calendar.google.com/calendar/embed?src=$calendarId"
|
||||
|
||||
override fun delete(): Mono<Boolean> {
|
||||
//Delete self from cache
|
||||
DiscalCache.handleCalendarDelete(guildId)
|
||||
|
||||
return CalendarWrapper.deleteCalendar(calendarData)
|
||||
.then(DatabaseManager.deleteCalendarAndRelatedData(calendarData))
|
||||
.thenReturn(true)
|
||||
}
|
||||
|
||||
override fun update(spec: UpdateCalendarSpec): Mono<UpdateCalendarResponse> {
|
||||
val content = GoogleCalendarModel()
|
||||
content.id = this.calendarId
|
||||
|
||||
spec.name?.let { content.summary = it }
|
||||
spec.description?.let { content.description = it }
|
||||
spec.timezone?.let { content.timeZone = it.id }
|
||||
|
||||
return CalendarWrapper.patchCalendar(content, this.calendarData)
|
||||
.timeout(Duration.ofSeconds(30))
|
||||
.flatMap { confirmed ->
|
||||
val rule = AclRule()
|
||||
.setScope(AclRule.Scope().setType("default"))
|
||||
.setRole("reader")
|
||||
|
||||
val new = GoogleCalendar(this.calendarData, confirmed)
|
||||
//Update cache
|
||||
DiscalCache.putCalendar(new)
|
||||
|
||||
return@flatMap AclRuleWrapper.insertRule(rule, this.calendarData)
|
||||
.thenReturn(UpdateCalendarResponse(
|
||||
old = this,
|
||||
new = new,
|
||||
success = true)
|
||||
)
|
||||
}.defaultIfEmpty(UpdateCalendarResponse(old = this, success = false))
|
||||
}
|
||||
|
||||
override fun getEvent(eventId: String): Mono<Event> {
|
||||
return GoogleEvent.get(this, eventId)
|
||||
}
|
||||
|
||||
override fun getUpcomingEvents(amount: Int): Flux<Event> {
|
||||
return EventWrapper.getEvents(calendarData, amount, System.currentTimeMillis())
|
||||
.flatMapMany(this::loadEvents)
|
||||
}
|
||||
|
||||
override fun getOngoingEvents(): Flux<Event> {
|
||||
val start = System.currentTimeMillis() - Duration.ofDays(14).toMillis() // 2 weeks ago
|
||||
val end = System.currentTimeMillis() + Duration.ofDays(1).toMillis() // One day from now
|
||||
|
||||
return EventWrapper.getEvents(calendarData, start, end)
|
||||
.flatMapMany { Flux.fromIterable(it) }
|
||||
.filter { it.start.asInstant(timezone).isBefore(Instant.now()) }
|
||||
.filter { it.end.asInstant(timezone).isAfter(Instant.now()) }
|
||||
.collectList()
|
||||
.flatMapMany(this::loadEvents)
|
||||
}
|
||||
|
||||
override fun getEventsInTimeRange(start: Instant, end: Instant): Flux<Event> {
|
||||
return EventWrapper.getEvents(calendarData, start.toEpochMilli(), end.toEpochMilli())
|
||||
.flatMapMany(this::loadEvents)
|
||||
}
|
||||
|
||||
override fun createEvent(spec: CreateEventSpec): Mono<Event> {
|
||||
val event = GoogleEventModel()
|
||||
event.id = KeyGenerator.generateEventId()
|
||||
event.visibility = "public"
|
||||
|
||||
spec.name?.let { event.summary = it }
|
||||
spec.description?.let { event.description = it }
|
||||
spec.location?.let { event.location = it }
|
||||
|
||||
|
||||
event.start = EventDateTime()
|
||||
.setDateTime(DateTime(spec.start.toEpochMilli()))
|
||||
.setTimeZone(this.timezone.id)
|
||||
event.end = EventDateTime()
|
||||
.setDateTime(DateTime(spec.end.toEpochMilli()))
|
||||
.setTimeZone(this.timezone.id)
|
||||
|
||||
if (spec.color != EventColor.NONE)
|
||||
event.colorId = spec.color.id.toString()
|
||||
|
||||
if (spec.recur)
|
||||
spec.recurrence?.let { event.recurrence = listOf(it.toRRule()) }
|
||||
|
||||
//Okay, all values are set, lets create the event now...
|
||||
return EventWrapper.createEvent(this.calendarData, event).flatMap { confirmed ->
|
||||
val data = EventData(
|
||||
this.guildId,
|
||||
confirmed.id,
|
||||
calendarNumber,
|
||||
spec.end.toEpochMilli(),
|
||||
spec.image.orEmpty()
|
||||
)
|
||||
|
||||
return@flatMap DatabaseManager.updateEventData(data)
|
||||
.thenReturn(GoogleEvent(this, data, confirmed))
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadEvents(events: List<GoogleEventModel>): Flux<GoogleEvent> {
|
||||
return DatabaseManager.getEventsData(guildId, events.map { it.id }).flatMapMany { data ->
|
||||
Flux.fromIterable(events).concatMap {
|
||||
val id = if (it.id.contains("_")) it.id.split("_")[0] else it.id
|
||||
|
||||
if (data.containsKey(id)) Mono.just(GoogleEvent(this, data[id]!!, it))
|
||||
else Mono.just(GoogleEvent(this, EventData(guildId, eventId = id), it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal companion object {
|
||||
/**
|
||||
* Requests to retrieve the [Calendar] from the provided [CalendarData]
|
||||
* If an error occurs, it is emitted through the [Mono]
|
||||
*
|
||||
* @param calData The data object for the Calendar to be built with
|
||||
* @return A [Mono] containing the [Calendar], if it does not exist, [empty][Mono.empty] is returned.
|
||||
*/
|
||||
fun get(calData: CalendarData): Mono<Calendar> {
|
||||
return CalendarWrapper.getCalendar(calData)
|
||||
.map { GoogleCalendar(calData, it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
package org.dreamexposure.discal.core.entities.google
|
||||
|
||||
import com.google.api.client.util.DateTime
|
||||
import com.google.api.services.calendar.model.EventDateTime
|
||||
import org.dreamexposure.discal.core.`object`.event.EventData
|
||||
import org.dreamexposure.discal.core.`object`.event.Recurrence
|
||||
import org.dreamexposure.discal.core.database.DatabaseManager
|
||||
import org.dreamexposure.discal.core.entities.Calendar
|
||||
import org.dreamexposure.discal.core.entities.Event
|
||||
import org.dreamexposure.discal.core.entities.response.UpdateEventResponse
|
||||
import org.dreamexposure.discal.core.entities.spec.update.UpdateEventSpec
|
||||
import org.dreamexposure.discal.core.enums.event.EventColor
|
||||
import org.dreamexposure.discal.core.wrapper.google.EventWrapper
|
||||
import reactor.core.publisher.Mono
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import com.google.api.services.calendar.model.Event as GoogleEventModel
|
||||
|
||||
class GoogleEvent internal constructor(
|
||||
override val calendar: Calendar,
|
||||
override val eventData: EventData,
|
||||
private val baseEvent: GoogleEventModel,
|
||||
) : Event {
|
||||
override val eventId: String
|
||||
get() = baseEvent.id
|
||||
|
||||
override val name: String
|
||||
get() = baseEvent.summary.orEmpty()
|
||||
|
||||
override val description: String
|
||||
get() = baseEvent.description.orEmpty()
|
||||
|
||||
override val location: String
|
||||
get() = baseEvent.location.orEmpty()
|
||||
|
||||
override val link: String
|
||||
get() = baseEvent.htmlLink.orEmpty()
|
||||
|
||||
override val color: EventColor
|
||||
get() {
|
||||
return if (baseEvent.colorId != null && baseEvent.colorId.isNotEmpty())
|
||||
EventColor.fromNameOrHexOrId(baseEvent.colorId)
|
||||
else
|
||||
EventColor.NONE
|
||||
}
|
||||
|
||||
override val start: Instant
|
||||
get() {
|
||||
return if (baseEvent.start.dateTime != null) {
|
||||
Instant.ofEpochMilli(baseEvent.start.dateTime.value)
|
||||
} else {
|
||||
Instant.ofEpochMilli(baseEvent.start.date.value)
|
||||
.plus(1, ChronoUnit.DAYS)
|
||||
.atZone(timezone)
|
||||
.truncatedTo(ChronoUnit.DAYS)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
.atZone(timezone)
|
||||
.toInstant()
|
||||
}
|
||||
}
|
||||
|
||||
override val end: Instant
|
||||
get() {
|
||||
return if (baseEvent.end.dateTime != null) {
|
||||
Instant.ofEpochMilli(baseEvent.end.dateTime.value)
|
||||
} else {
|
||||
Instant.ofEpochMilli(baseEvent.end.date.value)
|
||||
.plus(1, ChronoUnit.DAYS)
|
||||
.atZone(timezone)
|
||||
.truncatedTo(ChronoUnit.DAYS)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
.atZone(timezone)
|
||||
.toInstant()
|
||||
}
|
||||
}
|
||||
|
||||
override val recur: Boolean
|
||||
get() = baseEvent.recurrence != null && baseEvent.recurrence.isNotEmpty()
|
||||
|
||||
override val recurrence: Recurrence
|
||||
get() {
|
||||
return if (recur)
|
||||
Recurrence.fromRRule(baseEvent.recurrence[0])
|
||||
else
|
||||
Recurrence()
|
||||
}
|
||||
|
||||
override fun update(spec: UpdateEventSpec): Mono<UpdateEventResponse> {
|
||||
val event = GoogleEventModel()
|
||||
event.id = this.eventId
|
||||
|
||||
spec.name?.let { event.summary = it }
|
||||
spec.description?.let { event.description = it }
|
||||
spec.location?.let { event.location = it }
|
||||
|
||||
//Always update start/end so that we can safely handle all day events without DateTime by overwriting it
|
||||
if (spec.start != null) {
|
||||
event.start = EventDateTime()
|
||||
.setDateTime(DateTime(spec.start.toEpochMilli()))
|
||||
.setTimeZone(this.timezone.id)
|
||||
} else {
|
||||
event.start = EventDateTime()
|
||||
.setDateTime(DateTime(this.start.toEpochMilli()))
|
||||
.setTimeZone(this.timezone.id)
|
||||
}
|
||||
if (spec.end != null) {
|
||||
event.end = EventDateTime()
|
||||
.setDateTime(DateTime(spec.end.toEpochMilli()))
|
||||
.setTimeZone(this.timezone.id)
|
||||
} else {
|
||||
event.end = EventDateTime()
|
||||
.setDateTime(DateTime(this.end.toEpochMilli()))
|
||||
.setTimeZone(this.timezone.id)
|
||||
}
|
||||
|
||||
spec.color?.let {
|
||||
if (it == EventColor.NONE)
|
||||
event.colorId = null
|
||||
else
|
||||
event.colorId = it.id.toString()
|
||||
}
|
||||
|
||||
//Special recurrence handling
|
||||
if (spec.recur != null) {
|
||||
if (spec.recur) {
|
||||
//event now recurs, add the RRUle.
|
||||
spec.recurrence?.let { event.recurrence = listOf(it.toRRule()) }
|
||||
}
|
||||
} else {
|
||||
//Recur equals null, so it's not changing whether its recurring, so handle if RRule changes only
|
||||
spec.recurrence?.let { event.recurrence = listOf(it.toRRule()) }
|
||||
}
|
||||
|
||||
//Okay, all values are set, lets patch this event now...
|
||||
return EventWrapper.patchEvent(this.calendar.calendarData, event).flatMap { confirmed ->
|
||||
val data = EventData(
|
||||
this.guildId,
|
||||
confirmed.id,
|
||||
calendar.calendarNumber,
|
||||
confirmed.end.dateTime.value,
|
||||
spec.image ?: this.image
|
||||
)
|
||||
|
||||
return@flatMap DatabaseManager.updateEventData(data)
|
||||
.thenReturn(UpdateEventResponse(true, old = this, GoogleEvent(this.calendar, data, confirmed)))
|
||||
}.defaultIfEmpty(UpdateEventResponse(false, old = this))
|
||||
}
|
||||
|
||||
override fun delete(): Mono<Boolean> {
|
||||
return EventWrapper.deleteEvent(calendar.calendarData, eventId)
|
||||
.flatMap { success ->
|
||||
if (success) {
|
||||
Mono.`when`(
|
||||
DatabaseManager.deleteAnnouncementsForEvent(guildId, eventId),
|
||||
DatabaseManager.deleteEventData(eventId),
|
||||
).thenReturn(true)
|
||||
} else {
|
||||
Mono.just(false)
|
||||
}
|
||||
}.defaultIfEmpty(false)
|
||||
}
|
||||
|
||||
internal companion object {
|
||||
/**
|
||||
* Requests to retrieve the event with the provided ID from the provided [Calendar].
|
||||
* If an error occurs, it is emitted through the [Mono]
|
||||
*
|
||||
* @param calendar The [Calendar] this event exists on.
|
||||
* @param id The ID of the event
|
||||
* @return A [Mono] containing the event, if it does not exist, the mono is empty.
|
||||
*/
|
||||
fun get(calendar: Calendar, id: String): Mono<Event> {
|
||||
return EventWrapper.getEvent(calendar.calendarData, id)
|
||||
.flatMap { event ->
|
||||
DatabaseManager.getEventData(calendar.guildId, id)
|
||||
.map {
|
||||
GoogleEvent(calendar, it, event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package org.dreamexposure.discal.core.entities.response
|
||||
|
||||
import org.dreamexposure.discal.core.entities.Calendar
|
||||
|
||||
data class UpdateCalendarResponse(
|
||||
val success: Boolean,
|
||||
val old: Calendar? = null,
|
||||
val new: Calendar? = null,
|
||||
)
|
||||
@@ -1,9 +0,0 @@
|
||||
package org.dreamexposure.discal.core.entities.response
|
||||
|
||||
import org.dreamexposure.discal.core.entities.Event
|
||||
|
||||
data class UpdateEventResponse(
|
||||
val success: Boolean,
|
||||
val old: Event? = null,
|
||||
val new: Event? = null,
|
||||
)
|
||||
@@ -1,16 +0,0 @@
|
||||
package org.dreamexposure.discal.core.entities.spec.create
|
||||
|
||||
import org.dreamexposure.discal.core.enums.calendar.CalendarHost
|
||||
import java.time.ZoneId
|
||||
|
||||
data class CreateCalendarSpec(
|
||||
val host: CalendarHost,
|
||||
|
||||
val calNumber: Int,
|
||||
|
||||
val name: String,
|
||||
|
||||
val description: String? = null,
|
||||
|
||||
val timezone: ZoneId,
|
||||
)
|
||||
@@ -1,25 +0,0 @@
|
||||
package org.dreamexposure.discal.core.entities.spec.create
|
||||
|
||||
import org.dreamexposure.discal.core.`object`.event.Recurrence
|
||||
import org.dreamexposure.discal.core.enums.event.EventColor
|
||||
import java.time.Instant
|
||||
|
||||
data class CreateEventSpec(
|
||||
val name: String? = null,
|
||||
|
||||
val description: String? = null,
|
||||
|
||||
val start: Instant,
|
||||
|
||||
val end: Instant,
|
||||
|
||||
val color: EventColor = EventColor.NONE,
|
||||
|
||||
val location: String? = null,
|
||||
|
||||
val image: String? = null,
|
||||
|
||||
val recur: Boolean = false,
|
||||
|
||||
val recurrence: Recurrence? = null,
|
||||
)
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.dreamexposure.discal.core.entities.spec.update
|
||||
|
||||
import java.time.ZoneId
|
||||
|
||||
data class UpdateCalendarSpec(
|
||||
val name: String? = null,
|
||||
|
||||
val description: String? = null,
|
||||
|
||||
val timezone: ZoneId? = null,
|
||||
)
|
||||
@@ -1,25 +0,0 @@
|
||||
package org.dreamexposure.discal.core.entities.spec.update
|
||||
|
||||
import org.dreamexposure.discal.core.`object`.event.Recurrence
|
||||
import org.dreamexposure.discal.core.enums.event.EventColor
|
||||
import java.time.Instant
|
||||
|
||||
data class UpdateEventSpec(
|
||||
val name: String? = null,
|
||||
|
||||
val description: String? = null,
|
||||
|
||||
val start: Instant? = null,
|
||||
|
||||
val end: Instant? = null,
|
||||
|
||||
val color: EventColor? = null,
|
||||
|
||||
val location: String? = null,
|
||||
|
||||
val image: String? = null,
|
||||
|
||||
val recur: Boolean? = null,
|
||||
|
||||
val recurrence: Recurrence? = null
|
||||
)
|
||||
@@ -1,22 +0,0 @@
|
||||
package org.dreamexposure.discal.core.enums.announcement
|
||||
|
||||
enum class AnnouncementModifier {
|
||||
BEFORE, DURING, END;
|
||||
|
||||
companion object {
|
||||
fun isValid(value: String): Boolean {
|
||||
return value.equals("BEFORE", true)
|
||||
|| value.equals("B4", true)
|
||||
|| value.equals("DURING", true)
|
||||
|| value.equals("END", true)
|
||||
}
|
||||
|
||||
fun fromValue(value: String): AnnouncementModifier {
|
||||
return when (value.uppercase()) {
|
||||
"DURING" -> DURING
|
||||
"END" -> END
|
||||
else -> BEFORE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
@Serializable(with = AnnouncementStyleAsIntSerializer::class)
|
||||
@Deprecated("Use new subclassed enum in GuildSettings")
|
||||
enum class AnnouncementStyle(val value: Int = 1) {
|
||||
FULL(1), SIMPLE(2), EVENT(3);
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package org.dreamexposure.discal.core.enums.announcement
|
||||
|
||||
enum class AnnouncementType {
|
||||
UNIVERSAL, SPECIFIC, COLOR, RECUR;
|
||||
|
||||
companion object {
|
||||
fun isValid(value: String): Boolean {
|
||||
return value.equals("UNIVERSAL", true)
|
||||
|| value.equals("SPECIFIC", true)
|
||||
|| value.equals("COLOR", true)
|
||||
|| value.equals("COLOUR", true)
|
||||
|| value.equals("RECUR", true)
|
||||
}
|
||||
|
||||
fun fromValue(value: String): AnnouncementType {
|
||||
return when (value.uppercase()) {
|
||||
"SPECIFIC" -> SPECIFIC
|
||||
"COLOR" -> COLOR
|
||||
"COLOUR" -> COLOR
|
||||
"RECUR" -> RECUR
|
||||
else -> UNIVERSAL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package org.dreamexposure.discal.core.exceptions
|
||||
|
||||
class ApiException(override val message: String? = null, val exception: Exception? = null): Exception(message, exception)
|
||||
@@ -1,3 +0,0 @@
|
||||
package org.dreamexposure.discal.core.exceptions.google
|
||||
|
||||
class GoogleAuthCancelException : Exception()
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.dreamexposure.discal.core.extensions
|
||||
|
||||
import org.dreamexposure.discal.core.entities.Event
|
||||
import org.dreamexposure.discal.core.`object`.new.Event
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.time.temporal.TemporalAdjusters
|
||||
@@ -19,11 +19,7 @@ fun List<String>.asStringList(): String {
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
fun MutableList<String>.setFromString(strList: String) {
|
||||
this += strList.split(",").filter(String::isNotBlank)
|
||||
}
|
||||
|
||||
fun MutableList<Event>.groupByDate(): Map<ZonedDateTime, List<Event>> {
|
||||
fun List<Event>.groupByDate(): Map<ZonedDateTime, List<Event>> {
|
||||
if (this.isEmpty()) return emptyMap()
|
||||
|
||||
// Get a list of all distinct days events take place on (the first start date and the last end date)
|
||||
@@ -35,40 +31,13 @@ fun MutableList<Event>.groupByDate(): Map<ZonedDateTime, List<Event>> {
|
||||
.with(TemporalAdjusters.ofDateAdjuster { identity -> identity })
|
||||
|
||||
// Get a list of all days between the start and end dates
|
||||
val days = mutableListOf<ZonedDateTime>()
|
||||
val days = mutableListOf<ZonedDateTime>(startDate, endDate)
|
||||
var current = startDate
|
||||
while (current.isBefore(endDate)) {
|
||||
current = current.plusDays(1)
|
||||
days.add(current)
|
||||
}
|
||||
|
||||
/*
|
||||
// First get a list of distinct dates each event starts on
|
||||
var rawDates = this.map {
|
||||
ZonedDateTime.ofInstant(it.start, it.timezone).truncatedTo(ChronoUnit.DAYS)
|
||||
.with(TemporalAdjusters.ofDateAdjuster { identity -> identity })
|
||||
}.toList()
|
||||
|
||||
// Add days for multi-day events including end dates
|
||||
rawDates = rawDates.plus(this.asSequence().filter {
|
||||
it.isMultiDay()
|
||||
}.map {
|
||||
val start = ZonedDateTime.ofInstant(it.start, it.timezone).truncatedTo(ChronoUnit.DAYS)
|
||||
.with(TemporalAdjusters.ofDateAdjuster { identity -> identity })
|
||||
val end = ZonedDateTime.ofInstant(it.end, it.timezone).truncatedTo(ChronoUnit.DAYS)
|
||||
.with(TemporalAdjusters.ofDateAdjuster { identity -> identity })
|
||||
|
||||
val days = listOf<ZonedDateTime>()
|
||||
var current = start
|
||||
while (current.isBefore(end)) {
|
||||
current = current.plusDays(1)
|
||||
days.plus(current)
|
||||
}
|
||||
|
||||
days
|
||||
}.flatten())
|
||||
*/
|
||||
|
||||
// Sort dates
|
||||
val sortedDates = days.distinct().sorted().toList()
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.dreamexposure.discal.core.extensions
|
||||
|
||||
import org.dreamexposure.discal.core.enums.time.BadTimezone
|
||||
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.*
|
||||
@@ -21,24 +20,14 @@ fun String.toMarkdown(): String = GlobalVal.MARKDOWN_CONVERTER.convert(this.sani
|
||||
|
||||
fun String.unescapeNewLines(): String = this.replace(Regex("([\\\\\\n]+)(n)"), "\n").replace("=0D=0A", "\n")
|
||||
|
||||
fun String.isValidTimezone(): Boolean {
|
||||
return try {
|
||||
ZoneId.getAvailableZoneIds().contains(this) && !BadTimezone.isBad(this)
|
||||
} catch (ignore: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun String.toZoneId(): ZoneId? {
|
||||
return try {
|
||||
if (!BadTimezone.isBad(this)) ZoneId.of(this) else null
|
||||
} catch (ignore: Exception) {
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun String.isValidImage(allowGif: Boolean) = ImageValidator.validate(this, allowGif)
|
||||
|
||||
fun String.padCenter(length: Int, padChar: Char = ' '): String {
|
||||
if (this.length >= length) return this
|
||||
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
package org.dreamexposure.discal.core.extensions.discord4j
|
||||
|
||||
import discord4j.common.util.Snowflake
|
||||
import discord4j.core.`object`.entity.Guild
|
||||
import discord4j.core.`object`.entity.Role
|
||||
import discord4j.rest.entity.RestGuild
|
||||
import discord4j.rest.http.client.ClientException
|
||||
import io.netty.handler.codec.http.HttpResponseStatus
|
||||
import org.dreamexposure.discal.core.database.DatabaseManager
|
||||
import org.dreamexposure.discal.core.entities.Calendar
|
||||
import org.dreamexposure.discal.core.entities.spec.create.CreateCalendarSpec
|
||||
import org.dreamexposure.discal.core.`object`.GuildSettings
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
//Settings
|
||||
fun Guild.getSettings(): Mono<GuildSettings> = getRestGuild().getSettings()
|
||||
|
||||
//Calendars
|
||||
/**
|
||||
* Attempts to request whether this [Guild] has at least one [Calendar].
|
||||
* If an error occurs, it is emitted through the [Mono]
|
||||
*
|
||||
* @return A [Mono] containing whether this [Guild] has a [Calendar].
|
||||
*/
|
||||
fun Guild.hasCalendar(): Mono<Boolean> = getRestGuild().hasCalendar()
|
||||
|
||||
fun Guild.canAddCalendar(): Mono<Boolean> = getRestGuild().canAddCalendar()
|
||||
|
||||
fun Guild.determineNextCalendarNumber(): Mono<Int> = getRestGuild().determineNextCalendarNumber()
|
||||
|
||||
/**
|
||||
* Attempts to retrieve this [Guild]'s main [Calendar] (calendar 1, this guild's first/primary calendar)
|
||||
* If an error occurs, it is emitted through the [Mono]
|
||||
*
|
||||
* @return A [Mono] containing this [Guild]'s main [Calendar], if it does not exist, [empty][Mono.empty] is returned.
|
||||
*/
|
||||
fun Guild.getMainCalendar(): Mono<Calendar> = getRestGuild().getMainCalendar()
|
||||
|
||||
/**
|
||||
* Attempts to retrieve this [Guild]'s [Calendar] with the supplied index.
|
||||
* If an error occurs, it is emitted through the [Mono]
|
||||
*
|
||||
* @param calNumber The number of the [Calendar]. one-indexed
|
||||
* @return A [Mono] containing the [Calendar] with the supplied index, if it does not exist, [empty][Mono.empty] is
|
||||
* returned.
|
||||
*/
|
||||
fun Guild.getCalendar(calNumber: Int): Mono<Calendar> = getRestGuild().getCalendar(calNumber)
|
||||
|
||||
/**
|
||||
* Attempts to retrieve all [calendars][Calendar] belonging to this [Guild].
|
||||
* If an error occurs, it is emitted through the [Flux]
|
||||
*
|
||||
* @return A [Flux] containing all the [calendars][Calendar] belonging to this [Guild].
|
||||
*/
|
||||
fun Guild.getAllCalendars(): Flux<Calendar> = getRestGuild().getAllCalendars()
|
||||
|
||||
/**
|
||||
* Attempts to create a [Calendar] with the supplied information on a 3rd party host.
|
||||
* If an error occurs, it is emitted through the [Mono].
|
||||
*
|
||||
* @param spec The instructions for creating the [Calendar]
|
||||
* @return A [Mono] containing the newly created [Calendar]
|
||||
*/
|
||||
fun Guild.createCalendar(spec: CreateCalendarSpec): Mono<Calendar> = getRestGuild().createCalendar(spec)
|
||||
|
||||
fun Guild.getControlRole(): Mono<Role> {
|
||||
return getSettings().flatMap { settings ->
|
||||
if (settings.controlRole.equals("everyone", true))
|
||||
return@flatMap this.everyoneRole
|
||||
else {
|
||||
return@flatMap getRoleById(Snowflake.of(settings.controlRole))
|
||||
.onErrorResume(ClientException::class.java) {
|
||||
//If control role is deleted/not found, we reset it to everyone
|
||||
if (it.status == HttpResponseStatus.NOT_FOUND) {
|
||||
settings.controlRole = "everyone"
|
||||
DatabaseManager.updateSettings(settings).then(everyoneRole)
|
||||
} else
|
||||
everyoneRole
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Guild.getRestGuild(): RestGuild {
|
||||
return client.rest().restGuild(data)
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package org.dreamexposure.discal.core.extensions.discord4j
|
||||
|
||||
import discord4j.core.`object`.entity.Message
|
||||
import discord4j.core.event.domain.interaction.DeferrableInteractionEvent
|
||||
import discord4j.core.spec.EmbedCreateSpec
|
||||
import discord4j.core.spec.InteractionFollowupCreateSpec
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
fun DeferrableInteractionEvent.followup(embed: EmbedCreateSpec): Mono<Message> {
|
||||
val spec = InteractionFollowupCreateSpec.builder()
|
||||
.addEmbed(embed)
|
||||
.build()
|
||||
|
||||
return this.createFollowup(spec)
|
||||
}
|
||||
|
||||
fun DeferrableInteractionEvent.followup(message: String): Mono<Message> {
|
||||
val spec = InteractionFollowupCreateSpec.builder()
|
||||
.content(message)
|
||||
.build()
|
||||
|
||||
return this.createFollowup(spec)
|
||||
}
|
||||
|
||||
fun DeferrableInteractionEvent.followup(message: String, embed: EmbedCreateSpec): Mono<Message> {
|
||||
val spec = InteractionFollowupCreateSpec.builder()
|
||||
.content(message)
|
||||
.addEmbed(embed)
|
||||
.build()
|
||||
|
||||
return this.createFollowup(spec)
|
||||
}
|
||||
|
||||
fun DeferrableInteractionEvent.followupEphemeral(embed: EmbedCreateSpec): Mono<Message> {
|
||||
val spec = InteractionFollowupCreateSpec.builder()
|
||||
.addEmbed(embed)
|
||||
.ephemeral(true)
|
||||
.build()
|
||||
|
||||
return this.createFollowup(spec)
|
||||
}
|
||||
|
||||
fun DeferrableInteractionEvent.followupEphemeral(message: String): Mono<Message> {
|
||||
val spec = InteractionFollowupCreateSpec.builder()
|
||||
.content(message)
|
||||
.ephemeral(true)
|
||||
.build()
|
||||
|
||||
return this.createFollowup(spec)
|
||||
}
|
||||
|
||||
fun DeferrableInteractionEvent.followupEphemeral(message: String, embed: EmbedCreateSpec): Mono<Message> {
|
||||
val spec = InteractionFollowupCreateSpec.builder()
|
||||
.content(message)
|
||||
.addEmbed(embed)
|
||||
.ephemeral(true)
|
||||
.build()
|
||||
|
||||
return this.createFollowup(spec)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package org.dreamexposure.discal.core.extensions.discord4j
|
||||
|
||||
import discord4j.core.`object`.entity.Member
|
||||
import discord4j.rest.entity.RestMember
|
||||
import discord4j.rest.util.PermissionSet
|
||||
import java.util.function.Predicate
|
||||
|
||||
fun Member.hasPermissions(pred: Predicate<PermissionSet>) = getRestMember().hasPermissions(pred)
|
||||
|
||||
fun Member.hasElevatedPermissions() = getRestMember().hasElevatedPermissions()
|
||||
|
||||
fun Member.hasControlRole() = getRestMember().hasControlRole()
|
||||
|
||||
fun Member.getRestMember(): RestMember = client.rest().restMember(guildId, memberData)
|
||||
@@ -1,139 +0,0 @@
|
||||
package org.dreamexposure.discal.core.extensions.discord4j
|
||||
|
||||
import com.google.api.services.calendar.model.AclRule
|
||||
import discord4j.core.`object`.entity.Guild
|
||||
import discord4j.rest.entity.RestGuild
|
||||
import org.dreamexposure.discal.core.cache.DiscalCache
|
||||
import org.dreamexposure.discal.core.database.DatabaseManager
|
||||
import org.dreamexposure.discal.core.entities.Calendar
|
||||
import org.dreamexposure.discal.core.entities.google.GoogleCalendar
|
||||
import org.dreamexposure.discal.core.entities.spec.create.CreateCalendarSpec
|
||||
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.wrapper.google.AclRuleWrapper
|
||||
import org.dreamexposure.discal.core.wrapper.google.CalendarWrapper
|
||||
import org.dreamexposure.discal.core.wrapper.google.GoogleAuthWrapper
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
import com.google.api.services.calendar.model.Calendar as GoogleCalendarModel
|
||||
|
||||
//Settings
|
||||
fun RestGuild.getSettings(): Mono<GuildSettings> = DatabaseManager.getSettings(this.id)
|
||||
|
||||
//Calendars
|
||||
/**
|
||||
* Attempts to request whether this [Guild] has at least one [Calendar].
|
||||
* If an error occurs, it is emitted through the [Mono]
|
||||
*
|
||||
* @return A [Mono] containing whether this [Guild] has a [Calendar].
|
||||
*/
|
||||
fun RestGuild.hasCalendar(): Mono<Boolean> {
|
||||
return DatabaseManager.getAllCalendars(this.id).map(List<CalendarData>::isNotEmpty)
|
||||
}
|
||||
|
||||
fun RestGuild.canAddCalendar(): Mono<Boolean> {
|
||||
//Always check the live database and bypass cache
|
||||
return DatabaseManager.getCalendarCount(this.id)
|
||||
.flatMap { current ->
|
||||
if (current == 0) Mono.just(true)
|
||||
else getSettings().map { current < it.maxCalendars }
|
||||
}
|
||||
}
|
||||
|
||||
fun RestGuild.determineNextCalendarNumber(): Mono<Int> {
|
||||
return DatabaseManager.getAllCalendars(this.id)
|
||||
.map(List<CalendarData>::size)
|
||||
.map { it + 1 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to retrieve this [Guild]'s main [Calendar] (calendar 1, this guild's first/primary calendar)
|
||||
* If an error occurs, it is emitted through the [Mono]
|
||||
*
|
||||
* @return A [Mono] containing this [Guild]'s main [Calendar], if it does not exist, [empty][Mono.empty] is returned.
|
||||
*/
|
||||
fun RestGuild.getMainCalendar(): Mono<Calendar> = this.getCalendar(1)
|
||||
|
||||
/**
|
||||
* Attempts to retrieve this [Guild]'s [Calendar] with the supplied index.
|
||||
* If an error occurs, it is emitted through the [Mono]
|
||||
*
|
||||
* @param calNumber The number of the [Calendar]. one-indexed
|
||||
* @return A [Mono] containing the [Calendar] with the supplied index, if it does not exist, [empty][Mono.empty] is
|
||||
* returned.
|
||||
*/
|
||||
fun RestGuild.getCalendar(calNumber: Int): Mono<Calendar> {
|
||||
//Check cache first
|
||||
val cal = DiscalCache.getCalendar(id, calNumber)
|
||||
if (cal != null) return Mono.just(cal)
|
||||
|
||||
return DatabaseManager.getCalendar(this.id, calNumber)
|
||||
.flatMap(Calendar.Companion::from)
|
||||
.doOnNext(DiscalCache::putCalendar)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to retrieve all [calendars][Calendar] belonging to this [Guild].
|
||||
* If an error occurs, it is emitted through the [Flux]
|
||||
*
|
||||
* @return A [Flux] containing all the [calendars][Calendar] belonging to this [Guild].
|
||||
*/
|
||||
fun RestGuild.getAllCalendars(): Flux<Calendar> {
|
||||
//check cache first
|
||||
val cals = DiscalCache.getAllCalendars(id)
|
||||
if (cals != null) return Flux.fromIterable(cals)
|
||||
|
||||
return DatabaseManager.getAllCalendars(this.id)
|
||||
.flatMapMany { Flux.fromIterable(it) }
|
||||
.flatMap(Calendar.Companion::from)
|
||||
.doOnNext(DiscalCache::putCalendar)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to create a [Calendar] with the supplied information on a 3rd party host.
|
||||
* If an error occurs, it is emitted through the [Mono].
|
||||
*
|
||||
* @param spec The instructions for creating the [Calendar]
|
||||
* @return A [Mono] containing the newly created [Calendar]
|
||||
*/
|
||||
fun RestGuild.createCalendar(spec: CreateCalendarSpec): Mono<Calendar> {
|
||||
return Mono.defer {
|
||||
when (spec.host) {
|
||||
CalendarHost.GOOGLE -> {
|
||||
val credId = GoogleAuthWrapper.randomCredentialId()
|
||||
val googleCal = GoogleCalendarModel()
|
||||
|
||||
googleCal.summary = spec.name
|
||||
spec.description?.let { googleCal.description = it }
|
||||
googleCal.timeZone = spec.timezone.id
|
||||
|
||||
//Call google to create it
|
||||
CalendarWrapper.createCalendar(googleCal, credId, this.id)
|
||||
.timeout(Duration.ofSeconds(30))
|
||||
.flatMap { confirmed ->
|
||||
val data = CalendarData(
|
||||
guildId = this.id,
|
||||
calendarNumber = spec.calNumber,
|
||||
host = CalendarHost.GOOGLE,
|
||||
calendarId = confirmed.id,
|
||||
calendarAddress = confirmed.id,
|
||||
credentialId = credId
|
||||
)
|
||||
|
||||
val rule = AclRule()
|
||||
.setScope(AclRule.Scope().setType("default"))
|
||||
.setRole("reader")
|
||||
|
||||
Mono.`when`(
|
||||
DatabaseManager.updateCalendar(data),
|
||||
AclRuleWrapper.insertRule(rule, data)
|
||||
).thenReturn(GoogleCalendar(data, confirmed))
|
||||
.doOnNext(DiscalCache::putCalendar)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package org.dreamexposure.discal.core.extensions.discord4j
|
||||
|
||||
import discord4j.common.util.Snowflake
|
||||
import discord4j.discordjson.Id
|
||||
import discord4j.discordjson.json.MemberData
|
||||
import discord4j.discordjson.json.RoleData
|
||||
import discord4j.rest.entity.RestMember
|
||||
import discord4j.rest.util.Permission
|
||||
import discord4j.rest.util.PermissionSet
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import java.util.function.Predicate
|
||||
|
||||
fun RestMember.hasPermissions(pred: Predicate<PermissionSet>): Mono<Boolean> {
|
||||
return this.guild().data.flatMap { guildData ->
|
||||
if (guildData.ownerId().asLong() == this.id.asLong()) {
|
||||
Mono.just(true)
|
||||
} else {
|
||||
this.data.flatMap { memberData ->
|
||||
Flux.fromIterable(guildData.roles())
|
||||
.filter { memberData.roles().contains(it.id()) }
|
||||
.map(RoleData::permissions)
|
||||
.reduce(0L) { perm: Long, accumulator: Long -> accumulator or perm }
|
||||
.map(PermissionSet::of)
|
||||
.map(pred::test)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun RestMember.hasElevatedPermissions(): Mono<Boolean> {
|
||||
return hasPermissions() {
|
||||
it.contains(Permission.MANAGE_GUILD) || it.contains(Permission.ADMINISTRATOR)
|
||||
}
|
||||
}
|
||||
|
||||
fun RestMember.hasControlRole(): Mono<Boolean> {
|
||||
return this.guild().getSettings().flatMap { settings ->
|
||||
if (settings.controlRole.equals("everyone", true))
|
||||
return@flatMap Mono.just(true)
|
||||
|
||||
if (Snowflake.of(settings.controlRole).equals(settings.guildID)) // Also everyone (older guilds)
|
||||
return@flatMap Mono.just(true)
|
||||
|
||||
this.data
|
||||
.map(MemberData::roles)
|
||||
.flatMapMany { Flux.fromIterable(it) }
|
||||
.map(Id::asString)
|
||||
.collectList()
|
||||
.map { it.contains(settings.controlRole) }
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import org.dreamexposure.discal.core.serializers.SnowflakeAsStringSerializer
|
||||
import java.util.*
|
||||
|
||||
@Serializable
|
||||
@Deprecated("Prefer to use new.GuildSettings impl")
|
||||
data class GuildSettings(
|
||||
@Serializable(with = SnowflakeAsStringSerializer::class)
|
||||
@SerialName("guild_id")
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package org.dreamexposure.discal.core.`object`
|
||||
|
||||
import discord4j.common.util.Snowflake
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
@Serializable
|
||||
abstract class Pre(
|
||||
@Transient
|
||||
open val guildId: Snowflake = Snowflake.of(0),
|
||||
|
||||
@Transient
|
||||
open val editing: Boolean = false,
|
||||
) {
|
||||
@Transient
|
||||
var lastEdit: Instant = Instant.now()
|
||||
|
||||
open fun generateWarnings(settings: GuildSettings): List<String> = Collections.emptyList()
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package org.dreamexposure.discal.core.`object`
|
||||
|
||||
import discord4j.common.util.Snowflake
|
||||
import reactor.core.publisher.Flux
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
@Deprecated("This was dumb, what was I doing lmao")
|
||||
class Wizard<T: Pre> {
|
||||
private val active = ConcurrentHashMap<Snowflake, T>()
|
||||
|
||||
init {
|
||||
Flux.interval(Duration.ofMinutes(30))
|
||||
.map { removeOld() }
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
fun get(id: Snowflake): T? {
|
||||
val p = active[id]
|
||||
if (p != null) p.lastEdit = Instant.now()
|
||||
return p
|
||||
}
|
||||
|
||||
fun start(pre: T): T? = active.put(pre.guildId, pre)
|
||||
|
||||
fun remove(id: Snowflake): T? = active.remove(id)
|
||||
|
||||
private fun removeOld() {
|
||||
val toRemove = mutableListOf<Snowflake>()
|
||||
|
||||
active.forEach {
|
||||
if (Instant.now().isAfter(it.value.lastEdit.plus(30, ChronoUnit.MINUTES))) {
|
||||
toRemove.add(it.key)
|
||||
}
|
||||
}
|
||||
toRemove.forEach { active.remove(it) }
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.dreamexposure.discal.core.`object`.calendar
|
||||
|
||||
import com.google.api.services.calendar.model.Calendar
|
||||
import discord4j.core.`object`.entity.Message
|
||||
|
||||
data class CalendarCreatorResponse(
|
||||
val successful: Boolean,
|
||||
val edited: Boolean,
|
||||
val creatorMessage: Message?,
|
||||
val calendar: Calendar?
|
||||
)
|
||||
@@ -1,56 +0,0 @@
|
||||
package org.dreamexposure.discal.core.`object`.calendar
|
||||
|
||||
import discord4j.common.util.Snowflake
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import org.dreamexposure.discal.core.crypto.KeyGenerator
|
||||
import org.dreamexposure.discal.core.enums.calendar.CalendarHost
|
||||
import org.dreamexposure.discal.core.serializers.SnowflakeAsStringSerializer
|
||||
import java.time.Instant
|
||||
|
||||
@Serializable
|
||||
data class CalendarData(
|
||||
@Serializable(with = SnowflakeAsStringSerializer::class)
|
||||
@SerialName("guild_id")
|
||||
val guildId: Snowflake = Snowflake.of(0),
|
||||
@SerialName("calendar_number")
|
||||
val calendarNumber: Int = 1,
|
||||
val host: CalendarHost,
|
||||
@SerialName("calendar_id")
|
||||
val calendarId: String = "primary",
|
||||
@SerialName("calendar_address")
|
||||
val calendarAddress: String = "primary",
|
||||
val external: Boolean = false,
|
||||
|
||||
//secure values that should not be serialized
|
||||
@Transient
|
||||
val credentialId: Int = 0,
|
||||
@Transient
|
||||
var privateKey: String = KeyGenerator.csRandomAlphaNumericString(16),
|
||||
@Transient
|
||||
var encryptedAccessToken: String = "N/a",
|
||||
@Transient
|
||||
var encryptedRefreshToken: String = "N/a",
|
||||
@Transient
|
||||
var expiresAt: Instant = Instant.now()
|
||||
): Comparable<CalendarData> {
|
||||
constructor(guildId: Snowflake, calendarNumber: Int, host: CalendarHost, calendarId: String,
|
||||
calendarAddress: String, credentialId: Int) :
|
||||
this(guildId, calendarNumber, host, calendarId, calendarAddress, false, credentialId)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun empty(guildId: Snowflake, host: CalendarHost) = CalendarData(guildId, host = host)
|
||||
|
||||
fun emptyExternal(guildId: Snowflake, host: CalendarHost) = CalendarData(guildId, external = true, host = host)
|
||||
}
|
||||
|
||||
fun expired() = Instant.now().isAfter(expiresAt)
|
||||
|
||||
override fun compareTo(other: CalendarData): Int {
|
||||
if (this.calendarNumber > other.calendarNumber) return 1
|
||||
if (this.calendarNumber < other.calendarNumber) return -1
|
||||
return 0
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user