Just casually deleting loads of now unused old code. Feels good

This commit is contained in:
NovaFox161
2024-12-07 01:22:26 -06:00
parent 061c9544c3
commit 08e7ee54d9
39 changed files with 2 additions and 2637 deletions
@@ -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)
}
}
@@ -1,13 +0,0 @@
package org.dreamexposure.discal.client.config
import org.dreamexposure.discal.core.`object`.Wizard
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 eventWizard(): Wizard<PreEvent> = Wizard()
}
@@ -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`.new.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.interfaceStyle.branded)
builder.author(guild.name, Config.URL_BASE.getString(), guild.getIconUrl(Image.Format.PNG).orElse(GlobalVal.iconUrl))
else
builder.author(getCommonMsg("bot.name", settings.locale), Config.URL_BASE.getString(), GlobalVal.iconUrl)
return builder
}
fun getMessage(embed: String, key: String, settings: GuildSettings, vararg args: String) =
getEmbedMessage(embed, key, settings.locale, *args)
}
@@ -1,136 +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.Event
import org.dreamexposure.discal.core.enums.time.DiscordTimestampFormat.LONG_DATETIME
import org.dreamexposure.discal.core.extensions.*
import org.dreamexposure.discal.core.`object`.event.PreEvent
import org.dreamexposure.discal.core.`object`.new.GuildSettings
import org.dreamexposure.discal.core.utils.getCommonMsg
object EventEmbed : EmbedMaker {
@Deprecated("Prefer to use EmbedService impl")
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()
}
@Deprecated("Prefer to use EmbedService impl")
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.locale),
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.locale),
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.locale),
false
)
if (event.start != null) builder.addField(
getMessage("event", "wizard.field.start", settings),
event.start!!.humanReadableFull(event.timezone, settings.interfaceStyle.timeFormat),
true
) else builder.addField(
getMessage("event", "wizard.field.start", settings),
getCommonMsg("embed.unset", settings.locale),
true
)
if (event.end != null) builder.addField(
getMessage("event", "wizard.field.end", settings),
event.end!!.humanReadableFull(event.timezone, settings.interfaceStyle.timeFormat),
true
) else builder.addField(
getMessage("event", "wizard.field.end", settings),
getCommonMsg("embed.unset", settings.locale),
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.locale),
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,51 +0,0 @@
package org.dreamexposure.discal.core.cache
import discord4j.common.util.Snowflake
import org.dreamexposure.discal.core.entities.Calendar
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 -> 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() {
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)
}
}
@@ -2,25 +2,19 @@
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.config.Config
import org.dreamexposure.discal.core.enums.calendar.CalendarHost
import org.dreamexposure.discal.core.logger.LOGGER
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 {
@@ -52,131 +46,6 @@ object DatabaseManager {
fun disconnectFromMySQL() = pool.dispose()
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(
@@ -200,279 +69,6 @@ object DatabaseManager {
}.onErrorResume { Mono.empty() }
}
}
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 {
@@ -480,115 +76,6 @@ private object Queries {
val SELECT_API_KEY = """SELECT * FROM ${Tables.API}
WHERE API_KEY = ?
""".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 {
@@ -596,22 +83,4 @@ private object Tables {
@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"
}
@@ -1,232 +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
@Deprecated("Prefer to use new.Event via the new CalendarService")
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,182 +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
@Deprecated("Prefer to use new.Event via the new CalendarService")
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,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,3 +0,0 @@
package org.dreamexposure.discal.core.exceptions.google
class GoogleAuthCancelException : Exception()
@@ -19,10 +19,6 @@ fun List<String>.asStringList(): String {
return builder.toString()
}
fun MutableList<String>.setFromString(strList: String) {
this += strList.split(",").filter(String::isNotBlank)
}
fun List<Event>.groupByDate(): Map<ZonedDateTime, List<Event>> {
if (this.isEmpty()) return emptyMap()
@@ -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,14 +20,6 @@ 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 (_: Exception) {
false
}
}
fun String.toZoneId(): ZoneId? {
return try {
if (!BadTimezone.isBad(this)) ZoneId.of(this) else null
@@ -37,8 +28,6 @@ fun String.toZoneId(): ZoneId? {
}
}
fun String.isValidImage(allowGif: Boolean) = ImageValidator.validate(this, allowGif)
fun String.padCenter(length: Int, padChar: Char = ' '): String {
if (this.length >= length) return this
@@ -1,23 +0,0 @@
package org.dreamexposure.discal.core.extensions.discord4j
import discord4j.core.`object`.entity.Guild
import discord4j.rest.entity.RestGuild
import org.dreamexposure.discal.core.entities.Calendar
import reactor.core.publisher.Mono
//Calendars
/**
* 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.
*/
@Deprecated("Prefer to use new CalendarService")
fun Guild.getCalendar(calNumber: Int): Mono<Calendar> = getRestGuild().getCalendar(calNumber)
fun Guild.getRestGuild(): RestGuild {
return client.rest().restGuild(data)
}
@@ -1,66 +0,0 @@
package org.dreamexposure.discal.core.extensions.discord4j
import discord4j.core.event.domain.interaction.DeferrableInteractionEvent
import discord4j.core.`object`.entity.Message
import discord4j.core.spec.EmbedCreateSpec
import discord4j.core.spec.InteractionFollowupCreateSpec
import reactor.core.publisher.Mono
@Deprecated("lol")
fun DeferrableInteractionEvent.followup(embed: EmbedCreateSpec): Mono<Message> {
val spec = InteractionFollowupCreateSpec.builder()
.addEmbed(embed)
.build()
return this.createFollowup(spec)
}
@Deprecated("lol")
fun DeferrableInteractionEvent.followup(message: String): Mono<Message> {
val spec = InteractionFollowupCreateSpec.builder()
.content(message)
.build()
return this.createFollowup(spec)
}
@Deprecated("lol")
fun DeferrableInteractionEvent.followup(message: String, embed: EmbedCreateSpec): Mono<Message> {
val spec = InteractionFollowupCreateSpec.builder()
.content(message)
.addEmbed(embed)
.build()
return this.createFollowup(spec)
}
@Deprecated("lol")
fun DeferrableInteractionEvent.followupEphemeral(embed: EmbedCreateSpec): Mono<Message> {
val spec = InteractionFollowupCreateSpec.builder()
.addEmbed(embed)
.ephemeral(true)
.build()
return this.createFollowup(spec)
}
@Deprecated("lol")
fun DeferrableInteractionEvent.followupEphemeral(message: String): Mono<Message> {
val spec = InteractionFollowupCreateSpec.builder()
.content(message)
.ephemeral(true)
.build()
return this.createFollowup(spec)
}
@Deprecated("lol")
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,38 +0,0 @@
package org.dreamexposure.discal.core.extensions.discord4j
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 reactor.core.publisher.Mono
//Calendars
/**
* 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.
*/
@Deprecated("Prefer to use new CalendarService")
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.
*/
@Deprecated("Prefer to use new CalendarService")
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)
}
@@ -1,22 +0,0 @@
package org.dreamexposure.discal.core.`object`
import discord4j.common.util.Snowflake
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import org.dreamexposure.discal.core.`object`.new.GuildSettings
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,57 +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
@Deprecated("Use new CalendarMetadata implementation instead")
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
}
}
@@ -1,11 +0,0 @@
package org.dreamexposure.discal.core.`object`.event
import com.google.api.services.calendar.model.Event
import discord4j.core.`object`.entity.Message
data class EventCreatorResponse(
val successful: Boolean,
val event: Event?,
val creatorMessage: Message?,
val edited: Boolean,
)
@@ -1,29 +0,0 @@
package org.dreamexposure.discal.core.`object`.event
import discord4j.common.util.Snowflake
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.dreamexposure.discal.core.serializers.SnowflakeAsStringSerializer
@Serializable
data class EventData(
@Serializable(with = SnowflakeAsStringSerializer::class)
@SerialName("guild_id")
val guildId: Snowflake = Snowflake.of(0),
@SerialName("event_id")
val eventId: String = "",
@SerialName("calendar_number")
val calendarNumber: Int = 1,
@SerialName("event_end")
val eventEnd: Long = 0,
@SerialName("image_link")
val imageLink: String = ""
) {
fun shouldBeSaved(): Boolean {
return this.imageLink.isNotEmpty()
}
}
@@ -1,162 +0,0 @@
package org.dreamexposure.discal.core.`object`.event
import discord4j.common.util.Snowflake
import discord4j.core.`object`.entity.Guild
import org.dreamexposure.discal.core.entities.Calendar
import org.dreamexposure.discal.core.entities.Event
import org.dreamexposure.discal.core.entities.spec.create.CreateEventSpec
import org.dreamexposure.discal.core.entities.spec.update.UpdateEventSpec
import org.dreamexposure.discal.core.enums.event.EventColor
import org.dreamexposure.discal.core.extensions.discord4j.getCalendar
import org.dreamexposure.discal.core.`object`.Pre
import org.dreamexposure.discal.core.`object`.new.GuildSettings
import org.dreamexposure.discal.core.utils.getEmbedMessage
import reactor.core.publisher.Mono
import java.time.Duration
import java.time.Instant
import java.time.ZoneId
@Suppress("DataClassPrivateConstructor")
data class PreEvent private constructor(
override val guildId: Snowflake,
val eventId: String? = null,
val calNumber: Int,
val timezone: ZoneId,
override val editing: Boolean = false,
): Pre(guildId, editing) {
var name: String? = null
var description: String? = null
var start: Instant? = null
var end: Instant? = null
var color: EventColor = EventColor.NONE
var location: String? = null
var image: String? = null
var recurrence: Recurrence? = null
var event: Event? = null
fun hasRequiredValues(): Boolean {
return this.start != null
&& this.end != null
}
fun createSpec(): CreateEventSpec {
return CreateEventSpec(
name = name,
description = description,
start = start!!,
end = end!!,
color = color,
location = location,
image = image,
recur = recurrence != null,
recurrence = recurrence,
)
}
fun updateSpec(): UpdateEventSpec {
return UpdateEventSpec(
name = name,
description = description,
start = start,
end = end,
color = color,
location = location,
image = image,
recur = recurrence != null,
recurrence = recurrence,
)
}
override fun generateWarnings(settings: GuildSettings): List<String> {
val warnings = mutableListOf<String>()
if (name.isNullOrBlank()) {
warnings.add(getEmbedMessage("event", "warning.wizard.noName", settings.locale))
}
// Checking end time is not needed
if (start != null && start!!.isBefore(Instant.now())) {
warnings.add(getEmbedMessage("event", "warning.wizard.past", settings.locale))
}
if (this.start != null && this.end != null) {
if (Duration.between(start!!, end!!).toDays() > 30) {
warnings.add(getEmbedMessage("event", "warning.wizard.veryLong", settings.locale))
}
}
return warnings
}
companion object {
fun new(calendar: Calendar): PreEvent {
return PreEvent(
guildId = calendar.guildId,
calNumber = calendar.calendarNumber,
timezone = calendar.timezone,
editing = false,
)
}
fun edit(event: Event): PreEvent {
val pre = PreEvent(
guildId = event.guildId,
eventId = event.eventId,
calNumber = event.calendar.calendarNumber,
timezone = event.timezone,
editing = true
)
pre.name = event.name
pre.description = event.description
pre.start = event.start
pre.end = event.end
pre.color = event.color
pre.location = event.location
pre.image = event.image
if (event.recur)
pre.recurrence = event.recurrence
pre.event = event
return pre
}
fun copy(guild: Guild, event: Event, targetCalNum: Int): Mono<PreEvent> {
val calMono: Mono<Calendar> =
if (targetCalNum != event.calendar.calendarNumber) {
guild.getCalendar(targetCalNum).defaultIfEmpty(event.calendar)
} else {
Mono.just(event.calendar)
}
return calMono.map { targetCal ->
val pre = PreEvent(
guildId = event.guildId,
calNumber = targetCal.calendarNumber,
timezone = targetCal.timezone,
)
pre.name = event.name
pre.description = event.description
pre.start = event.start
pre.end = event.end
pre.color = event.color
pre.location = event.location
pre.image = event.image
if (event.recur)
pre.recurrence = event.recurrence
pre
}
}
}
}
@@ -1,15 +0,0 @@
package org.dreamexposure.discal.core.`object`.google
import discord4j.core.`object`.entity.User
import org.dreamexposure.discal.core.`object`.GuildSettings
import reactor.core.publisher.Mono
data class ExternalGoogleAuthPoll(
val user: User,
val settings: GuildSettings,
override var interval: Int,
override val expiresIn: Int,
override var remainingSeconds: Int,
override val deviceCode: String,
override val callback: ((poll: GoogleAuthPoll) -> Mono<Void>)
): GoogleAuthPoll(interval, expiresIn, remainingSeconds, deviceCode, callback)
@@ -1,11 +0,0 @@
package org.dreamexposure.discal.core.`object`.google
import reactor.core.publisher.Mono
open class GoogleAuthPoll(
open var interval: Int,
open val expiresIn: Int,
open var remainingSeconds: Int,
open val deviceCode: String,
open val callback: ((poll: GoogleAuthPoll) -> Mono<Void>),
)
@@ -1,10 +0,0 @@
package org.dreamexposure.discal.core.`object`.google
import java.time.Instant
data class GoogleCredentialData(
val credentialNumber: Int,
var encryptedRefreshToken: String,
var encryptedAccessToken: String,
var expiresAt: Instant,
)
@@ -1,12 +0,0 @@
package org.dreamexposure.discal.core.`object`.google
import reactor.core.publisher.Mono
data class InternalGoogleAuthPoll(
val credNumber: Int,
override var interval: Int,
override val expiresIn: Int,
override var remainingSeconds: Int,
override val deviceCode: String,
override val callback: ((poll: GoogleAuthPoll) -> Mono<Void>)
): GoogleAuthPoll(interval, expiresIn, remainingSeconds, deviceCode, callback)
@@ -1,13 +1,8 @@
package org.dreamexposure.discal.core.`object`.rest
import kotlinx.serialization.Serializable
import reactor.core.publisher.Mono
@Serializable
data class GenericResponse(
val message: String,
) {
fun asMono(): Mono<GenericResponse> = Mono.just(this)
}
)
@@ -1,17 +0,0 @@
package org.dreamexposure.discal.core.serializers
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.util.*
object UUIDasStringSerializer : KSerializer<UUID> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: UUID) = encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): UUID = UUID.fromString(decoder.decodeString())
}
@@ -1,17 +0,0 @@
package org.dreamexposure.discal.core.serializers
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.time.ZoneId
object ZoneIdAsStringSerializer : KSerializer<ZoneId> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ZoneId", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: ZoneId) = encoder.encodeString(value.id)
override fun deserialize(decoder: Decoder): ZoneId = ZoneId.of(decoder.decodeString())
}
@@ -7,7 +7,6 @@ import io.github.furstenheim.CopyDown
import io.github.furstenheim.OptionsBuilder
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import org.jsoup.safety.Safelist
import org.slf4j.Marker
import org.slf4j.MarkerFactory
@@ -32,9 +31,6 @@ object GlobalVal {
val JSON = "application/json; charset=utf-8".toMediaType()
@Deprecated("Use DI instead")
val HTTP_CLIENT: OkHttpClient = OkHttpClient()
val JSON_FORMAT = Json {
encodeDefaults = true
ignoreUnknownKeys = true
@@ -1,25 +0,0 @@
package org.dreamexposure.discal.core.wrapper.google
import com.google.api.services.calendar.model.AclRule
import org.dreamexposure.discal.core.logger.LOGGER
import org.dreamexposure.discal.core.`object`.calendar.CalendarData
import org.dreamexposure.discal.core.utils.GlobalVal
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
@Deprecated("Use the new GoogleCalendarApiWrapper component")
object AclRuleWrapper {
fun insertRule(rule: AclRule, calData: CalendarData): Mono<AclRule> {
return GoogleAuthWrapper.getCalendarService(calData).flatMap { service ->
Mono.fromCallable {
service.acl()
.insert(calData.calendarId, rule)
.setQuotaUser(calData.guildId.asString())
.execute()
}.subscribeOn(Schedulers.boundedElastic())
}.doOnError {
LOGGER.error(GlobalVal.DEFAULT, "[G.Cal] ACLRule insert failure", it)
}.onErrorResume { Mono.empty() }
}
}
@@ -1,98 +0,0 @@
package org.dreamexposure.discal.core.wrapper.google
import com.google.api.services.calendar.model.Calendar
import com.google.api.services.calendar.model.CalendarListEntry
import discord4j.common.util.Snowflake
import org.dreamexposure.discal.core.logger.LOGGER
import org.dreamexposure.discal.core.`object`.calendar.CalendarData
import org.dreamexposure.discal.core.utils.GlobalVal
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
@Deprecated("User new GoogleCalendarApiWrapper component")
object CalendarWrapper {
fun createCalendar(calendar: Calendar, credId: Int, guildId: Snowflake): Mono<Calendar> {
return GoogleAuthWrapper.getCalendarService(credId).flatMap { service ->
Mono.fromCallable {
service.calendars()
.insert(calendar)
.setQuotaUser(guildId.asString())
.execute()
}.subscribeOn(Schedulers.boundedElastic())
}.doOnError {
LOGGER.error(GlobalVal.DEFAULT, "[G.Cal] Calendar create failure", it)
}.onErrorResume { Mono.empty() }
}
fun patchCalendar(calendar: Calendar, calData: CalendarData): Mono<Calendar> {
return GoogleAuthWrapper.getCalendarService(calData).flatMap { service ->
Mono.fromCallable {
service.calendars()
.patch(calendar.id, calendar)
.setQuotaUser(calData.guildId.asString())
.execute()
}.subscribeOn(Schedulers.boundedElastic())
}.doOnError {
LOGGER.error(GlobalVal.DEFAULT, "[G.Cal] Calendar patch failure", it)
}.onErrorResume { Mono.empty() }
}
fun updateCalendar(calendar: Calendar, calData: CalendarData): Mono<Calendar> {
return GoogleAuthWrapper.getCalendarService(calData).flatMap { service ->
Mono.fromCallable {
service.calendars()
.update(calendar.id, calendar)
.setQuotaUser(calData.guildId.asString())
.execute()
}.subscribeOn(Schedulers.boundedElastic())
}.doOnError {
LOGGER.error(GlobalVal.DEFAULT, "[G.Cal] Calendar update failure", it)
}.onErrorResume { Mono.empty() }
}
fun getCalendar(calData: CalendarData): Mono<Calendar> {
return GoogleAuthWrapper.getCalendarService(calData).flatMap { service ->
Mono.fromCallable {
service.calendars()
.get(calData.calendarAddress)
.setQuotaUser(calData.guildId.asString())
.execute()
}.subscribeOn(Schedulers.boundedElastic())
}.doOnError {
LOGGER.error(GlobalVal.DEFAULT, "[G.Cal] Calendar get failure", it)
}.onErrorResume { Mono.empty() }
}
fun deleteCalendar(calData: CalendarData): Mono<Boolean> {
return Mono.just(calData)
.filter { !it.external }
.filter { !it.calendarAddress.equals("primary", true) }
.flatMap { GoogleAuthWrapper.getCalendarService(calData) }
.flatMap { service ->
Mono.fromCallable {
service.calendars()
.delete(calData.calendarAddress)
.setQuotaUser(calData.guildId.asString())
.execute()
}.subscribeOn(Schedulers.boundedElastic())
}.thenReturn(true)
.doOnError {
LOGGER.error(GlobalVal.DEFAULT, "[G.Cal] Calendar delete failure", it)
}.onErrorReturn(false)
}
fun getUsersExternalCalendars(calData: CalendarData): Mono<List<CalendarListEntry>> {
return GoogleAuthWrapper.getCalendarService(calData).flatMap { service ->
Mono.fromCallable {
service.calendarList()
.list()
.setMinAccessRole("writer")
.setQuotaUser(calData.guildId.asString())
.execute()
.items
}.subscribeOn(Schedulers.boundedElastic())
}.doOnError {
LOGGER.error(GlobalVal.DEFAULT, "[G.Cal] External calendar list failure", it)
}.onErrorResume { Mono.empty() }
}
}
@@ -1,181 +0,0 @@
package org.dreamexposure.discal.core.wrapper.google
import com.google.api.client.googleapis.json.GoogleJsonResponseException
import com.google.api.client.util.DateTime
import com.google.api.services.calendar.Calendar
import com.google.api.services.calendar.model.Event
import org.apache.http.HttpStatus
import org.dreamexposure.discal.core.database.DatabaseManager
import org.dreamexposure.discal.core.logger.LOGGER
import org.dreamexposure.discal.core.`object`.calendar.CalendarData
import org.dreamexposure.discal.core.utils.GlobalVal
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
import java.util.function.Predicate
@Deprecated("Use new GoogleCalendarApiWrapper component")
object EventWrapper {
fun createEvent(calData: CalendarData, event: Event): Mono<Event> {
return GoogleAuthWrapper.getCalendarService(calData).flatMap { service ->
Mono.fromCallable {
service.events()
.insert(calData.calendarId, event)
.setQuotaUser(calData.guildId.asString())
.execute()
}.subscribeOn(Schedulers.boundedElastic())
}.doOnError {
LOGGER.error(GlobalVal.DEFAULT, "[G.Cal] Event create failure", it)
}.onErrorResume { Mono.empty() }
}
fun patchEvent(calData: CalendarData, event: Event): Mono<Event> {
return GoogleAuthWrapper.getCalendarService(calData).flatMap { service ->
Mono.fromCallable {
service.events()
.patch(calData.calendarId, event.id, event)
.setQuotaUser(calData.guildId.asString())
.execute()
}.subscribeOn(Schedulers.boundedElastic())
}.doOnError {
LOGGER.error(GlobalVal.DEFAULT, "[G.Cal] Event patch failure", it)
}.onErrorResume { Mono.empty() }
}
fun updateEvent(calData: CalendarData, event: Event): Mono<Event> {
return GoogleAuthWrapper.getCalendarService(calData).flatMap { service ->
Mono.fromCallable {
service.events()
.update(calData.calendarId, event.id, event)
.setQuotaUser(calData.guildId.asString())
.execute()
}.subscribeOn(Schedulers.boundedElastic())
}.doOnError {
LOGGER.error(GlobalVal.DEFAULT, "[G.Cal] Event update failure", it)
}.onErrorResume { Mono.empty() }
}
fun getEvent(calData: CalendarData, id: String): Mono<Event> {
return GoogleAuthWrapper.getCalendarService(calData).flatMap { service ->
Mono.fromCallable {
service.events()
.get(calData.calendarId, id)
.setQuotaUser(calData.guildId.asString())
.execute()
}.filterWhen {
/*
Don't show "deleted" events
See "status" flag: https://developers.google.com/calendar/api/v3/reference/events#resource
*/
if (it.status.equals("cancelled", true)) {
// Delete any announcements tied to it.
DatabaseManager.deleteAnnouncementsForEvent(calData.guildId, id).thenReturn(false)
} else Mono.just(true)
}.subscribeOn(Schedulers.boundedElastic())
}.onErrorResume(GoogleJsonResponseException::class.java) {
return@onErrorResume when (it.statusCode) {
HttpStatus.SC_GONE -> {
// The event is gone. Sometimes google will return this if the event is deleted.
DatabaseManager.deleteAnnouncementsForEvent(calData.guildId, id).then(Mono.empty())
}
HttpStatus.SC_NOT_FOUND -> {
// Event not found. Was this ever an event?
DatabaseManager.deleteAnnouncementsForEvent(calData.guildId, id).then(Mono.empty())
}
else -> {
LOGGER.error(GlobalVal.DEFAULT, "[G.Cal] Event get failure", it)
Mono.empty()
}
}
}.onErrorResume { Mono.empty() } //Can safely ignore this, the event just doesn't exist.
}
fun getEvents(calData: CalendarData, amount: Int, start: Long): Mono<List<Event>> {
return GoogleAuthWrapper.getCalendarService(calData).flatMap { service ->
Mono.fromCallable {
service.events()
.list(calData.calendarId)
.setMaxResults(amount)
.setTimeMin(DateTime(start))
.setOrderBy("startTime")
.setSingleEvents(true)
.setShowDeleted(false)
.setQuotaUser(calData.guildId.asString())
.execute()
.items
}.subscribeOn(Schedulers.boundedElastic())
}.doOnError {
LOGGER.error(GlobalVal.DEFAULT, "[G.Cal] Event list(1) failure", it)
}.onErrorResume { Mono.empty() }
}
fun getEvents(calData: CalendarData, amount: Int, start: Long, end: Long): Mono<List<Event>> {
return GoogleAuthWrapper.getCalendarService(calData).flatMap { service: Calendar ->
Mono.fromCallable {
service.events()
.list(calData.calendarId)
.setMaxResults(amount)
.setTimeMin(DateTime(start))
.setTimeMax(DateTime(end))
.setOrderBy("startTime")
.setSingleEvents(true)
.setShowDeleted(false)
.setQuotaUser(calData.guildId.asString())
.execute().items
}.subscribeOn(Schedulers.boundedElastic())
}.doOnError {
LOGGER.error(GlobalVal.DEFAULT, "[G.Cal] Event list(3) failure", it)
}.onErrorResume { Mono.empty() }
}
fun getEvents(calData: CalendarData, start: Long, end: Long): Mono<List<Event>> {
return GoogleAuthWrapper.getCalendarService(calData).flatMap { service: Calendar ->
Mono.fromCallable {
service.events()
.list(calData.calendarId)
.setTimeMin(DateTime(start))
.setTimeMax(DateTime(end))
.setOrderBy("startTime")
.setSingleEvents(true)
.setShowDeleted(false)
.setQuotaUser(calData.guildId.asString())
.execute().items
}.subscribeOn(Schedulers.boundedElastic())
}.doOnError {
LOGGER.error(GlobalVal.DEFAULT, "[G.Cal] Event list(4) failure", it)
}.onErrorResume { Mono.empty() }
}
fun deleteEvent(calData: CalendarData, id: String?): Mono<Boolean> {
return GoogleAuthWrapper.getCalendarService(calData).flatMap { service: Calendar ->
Mono.fromCallable {
val response = service.events()
.delete(calData.calendarAddress, id)
.setQuotaUser(calData.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 -> {
return@fromCallable true
}
404, 410 -> {
return@fromCallable false
}
else -> {
//Log response data and return false as google sent an unexpected response code.
LOGGER.debug(GlobalVal.DEFAULT, "Event delete error | ${response.statusCode} | ${response.statusMessage}")
return@fromCallable false
}
}
}.subscribeOn(Schedulers.boundedElastic())
}.doOnError(GoogleJsonResponseException::class.java) {
if (it.statusCode != 410 || it.statusCode != 404) {
LOGGER.error(GlobalVal.DEFAULT, "[G.Cal] Event delete failure", it)
}
}.doOnError(Predicate.not(GoogleJsonResponseException::class.java::isInstance)) {
LOGGER.error(GlobalVal.DEFAULT, "[G.Cal] Event delete failure", it)
}.onErrorReturn(false)
}
}
@@ -1,169 +0,0 @@
package org.dreamexposure.discal.core.wrapper.google
import com.google.api.client.auth.oauth2.Credential
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential
import com.google.api.client.http.HttpStatusCodes
import com.google.api.client.http.javanet.NetHttpTransport
import com.google.api.client.json.gson.GsonFactory
import discord4j.common.util.Snowflake
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Request
import org.dreamexposure.discal.core.config.Config
import org.dreamexposure.discal.core.database.DatabaseManager
import org.dreamexposure.discal.core.enums.calendar.CalendarHost
import org.dreamexposure.discal.core.exceptions.EmptyNotAllowedException
import org.dreamexposure.discal.core.logger.LOGGER
import org.dreamexposure.discal.core.`object`.calendar.CalendarData
import org.dreamexposure.discal.core.`object`.network.discal.CredentialData
import org.dreamexposure.discal.core.`object`.rest.RestError
import org.dreamexposure.discal.core.utils.GlobalVal.DEFAULT
import org.dreamexposure.discal.core.utils.GlobalVal.HTTP_CLIENT
import org.dreamexposure.discal.core.utils.GlobalVal.JSON_FORMAT
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
import java.util.concurrent.ConcurrentHashMap
import kotlin.random.Random
import com.google.api.services.calendar.Calendar as GoogleCalendarService
@Deprecated("Use GoogleCalendarApiWrapper")
object GoogleAuthWrapper {
private val discalTokens: MutableMap<Int, CredentialData> = ConcurrentHashMap()
private val externalTokens: MutableMap<Snowflake, CredentialData> = ConcurrentHashMap()
private fun authorize(credentialId: Int): Mono<Credential> {
return getAccessToken(credentialId)
.map(GoogleCredential()::setAccessToken)
.ofType(Credential::class.java) //Cast down to the class it extends
.switchIfEmpty(Mono.error(EmptyNotAllowedException()))
}
private fun authorize(calData: CalendarData): Mono<Credential> {
return Mono.just(calData).filter { !"N/a".equals(calData.encryptedAccessToken, true) }
.flatMap(this::getAccessToken)
.map(GoogleCredential()::setAccessToken)
.ofType(Credential::class.java) //Cast down to the class it extends
.switchIfEmpty(Mono.error(EmptyNotAllowedException()))
}
private fun buildService(credential: Credential): GoogleCalendarService {
return GoogleCalendarService.Builder(NetHttpTransport(), GsonFactory.getDefaultInstance(), credential)
.setApplicationName("DisCal")
.build()
}
private fun getAccessToken(credentialId: Int): Mono<String> {
val token = discalTokens[credentialId]
if (token != null && !token.isExpired()) {
return Mono.just(token.accessToken)
}
LOGGER.debug("Refreshing local token via CAM | credentialId:$credentialId")
return Mono.fromCallable {
val url = "${Config.URL_CAM.getString()}/v1/token".toHttpUrlOrNull()!!.newBuilder()
.addQueryParameter("host", CalendarHost.GOOGLE.name)
.addQueryParameter("id", credentialId.toString())
.build()
val request = Request.Builder().get()
.header("Authorization", "Int ${Config.SECRET_DISCAL_API_KEY.getString()}")
.url(url)
.build()
HTTP_CLIENT.newCall(request).execute()
}.subscribeOn(Schedulers.boundedElastic()).flatMap { response ->
when (response.code) {
HttpStatusCodes.STATUS_CODE_OK -> {
val data = JSON_FORMAT.decodeFromString(CredentialData.serializer(), response.body!!.string())
response.body?.close()
response.close()
discalTokens[credentialId] = data
Mono.just(data.accessToken)
}
else -> {
val error = JSON_FORMAT.decodeFromString(RestError.serializer(), response.body!!.string())
response.body?.close()
response.close()
// Log because this really shouldn't be happening
LOGGER.error(DEFAULT, "[Google] Error requesting access token from CAM for Int. | credentialId:$credentialId | error:$error")
Mono.empty()
}
}
}.doOnError {
LOGGER.error("GoogleAuth | Failed to get access token from CAM | credentialId:$credentialId", it)
}.onErrorResume { Mono.empty() }
}
private fun getAccessToken(calData: CalendarData): Mono<String> {
val token = externalTokens[calData.guildId]
if (token != null && !token.isExpired()) {
return Mono.just(token.accessToken)
}
LOGGER.debug("Refreshing local token via CAM | guild:{} | host:{} | calendarId:{} ", calData.guildId, calData.host, calData.calendarNumber)
return Mono.fromCallable {
val url = "${Config.URL_CAM.getString()}/v1/token".toHttpUrlOrNull()!!.newBuilder()
.addQueryParameter("host", calData.host.name)
.addQueryParameter("guild", calData.guildId.asString())
.addQueryParameter("id", calData.calendarNumber.toString())
.build()
val request = Request.Builder().get()
.header("Authorization", "Int ${Config.SECRET_DISCAL_API_KEY.getString()}")
.url(url)
.build()
HTTP_CLIENT.newCall(request).execute()
}.subscribeOn(Schedulers.boundedElastic()).flatMap { response ->
when (response.code) {
HttpStatusCodes.STATUS_CODE_OK -> {
val data = JSON_FORMAT.decodeFromString(CredentialData.serializer(), response.body!!.string())
response.body?.close()
response.close()
externalTokens[calData.guildId] = data
Mono.just(data.accessToken)
}
else -> {
val error = JSON_FORMAT.decodeFromString(RestError.serializer(), response.body!!.string())
response.body?.close()
response.close()
when (error) {
RestError.ACCESS_REVOKED -> {
// Delete calendar, user MUST reauthorize discal as the refresh token isn't valid.
DatabaseManager.deleteCalendarAndRelatedData(calData).then(Mono.empty())
}
else -> {
//An unknown/unsupported error has occurred, log and return empty, upstream can handle this
LOGGER.debug(DEFAULT, "[Google] Error requesting access token from CAM for Ext. | {}", error)
Mono.empty()
}
}
}
}
}
}
fun getCalendarService(calData: CalendarData): Mono<GoogleCalendarService> {
return Mono.defer {
if (calData.external) {
authorize(calData).map(this::buildService)
} else {
getCalendarService(calData.credentialId)
}
}.switchIfEmpty(Mono.error(EmptyNotAllowedException()))
}
fun getCalendarService(credentialId: Int): Mono<GoogleCalendarService> {
return authorize(credentialId)
.map(this::buildService)
.switchIfEmpty(Mono.error(EmptyNotAllowedException()))
}
fun randomCredentialId() = Random.nextInt(Config.SECRET_GOOGLE_CREDENTIAL_COUNT.getInt())
}