add static calendar overview message

Still need to include triggered updates and some other important things
This commit is contained in:
NovaFox161
2021-11-21 19:06:24 -06:00
parent ff96c61480
commit 9296abcabb
7 changed files with 337 additions and 5 deletions

View File

@@ -0,0 +1,76 @@
package org.dreamexposure.discal.client.commands
import discord4j.core.`object`.command.ApplicationCommandInteractionOption
import discord4j.core.`object`.command.ApplicationCommandInteractionOptionValue
import discord4j.core.`object`.entity.Member
import discord4j.core.`object`.entity.Message
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent
import discord4j.core.spec.MessageCreateSpec
import org.dreamexposure.discal.client.message.embed.CalendarEmbed
import org.dreamexposure.discal.core.`object`.GuildSettings
import org.dreamexposure.discal.core.`object`.StaticMessage
import org.dreamexposure.discal.core.database.DatabaseManager
import org.dreamexposure.discal.core.extensions.discord4j.followupEphemeral
import org.dreamexposure.discal.core.extensions.discord4j.getCalendar
import org.dreamexposure.discal.core.extensions.discord4j.hasElevatedPermissions
import org.dreamexposure.discal.core.utils.getCommonMsg
import org.springframework.stereotype.Component
import reactor.core.publisher.Mono
import java.time.Instant
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
@Component
class DisplayCalendarCommand : SlashCommand {
override val name = "displaycal"
override val ephemeral = true
override fun handle(event: ChatInputInteractionEvent, settings: GuildSettings): Mono<Message> {
val hour = event.getOption("time")
.flatMap(ApplicationCommandInteractionOption::getValue)
.map(ApplicationCommandInteractionOptionValue::asLong)
.get()
val calendarNumber = event.getOption("calendar")
.flatMap(ApplicationCommandInteractionOption::getValue)
.map(ApplicationCommandInteractionOptionValue::asLong)
.map(Long::toInt)
.orElse(1)
return Mono.justOrEmpty(event.interaction.member).filterWhen(Member::hasElevatedPermissions).flatMap {
event.interaction.guild.flatMap { guild ->
guild.getCalendar(calendarNumber).flatMap { cal ->
CalendarEmbed.overview(guild, settings, cal, true).flatMap { embed ->
event.interaction.channel.flatMap {
it.createMessage(MessageCreateSpec.builder()
.addEmbed(embed)
.build()
).flatMap { msg ->
val nextUpdate = ZonedDateTime.now(cal.timezone)
.truncatedTo(ChronoUnit.DAYS)
.plusHours(hour + 24)
.toInstant()
val staticMsg = StaticMessage(
settings.guildID,
msg.id,
msg.channelId,
StaticMessage.Type.CALENDAR_OVERVIEW,
Instant.now(),
nextUpdate,
calendarNumber,
)
DatabaseManager.updateStaticMessage(staticMsg)
.then(event.followupEphemeral(getCommonMsg("success.generic", settings)))
}
}
}
}.switchIfEmpty(event.followupEphemeral(getCommonMsg("notFound.calendar", settings)))
}
}.switchIfEmpty(event.followupEphemeral(getCommonMsg("error.perms.elevated", settings)))
}
}

View File

@@ -0,0 +1,80 @@
package org.dreamexposure.discal.client.service
import discord4j.core.`object`.entity.Guild
import discord4j.core.spec.MessageEditSpec
import discord4j.rest.http.client.ClientException
import org.dreamexposure.discal.Application
import org.dreamexposure.discal.Application.Companion.getShardIndex
import org.dreamexposure.discal.client.DisCalClient
import org.dreamexposure.discal.client.message.embed.CalendarEmbed
import org.dreamexposure.discal.core.`object`.StaticMessage
import org.dreamexposure.discal.core.database.DatabaseManager
import org.dreamexposure.discal.core.extensions.discord4j.getCalendar
import org.dreamexposure.discal.core.extensions.discord4j.getSettings
import org.dreamexposure.discal.core.logger.LOGGER
import org.dreamexposure.discal.core.utils.GlobalVal
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
import org.springframework.stereotype.Component
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.function.TupleUtils
import java.time.Duration
import java.time.Instant
import java.time.temporal.ChronoUnit
@Component
class StaticMessageService: ApplicationRunner {
//TODO: use gateway client from DI once available
override fun run(args: ApplicationArguments?) {
Flux.interval(Duration.ofHours(1))
.flatMap { doMessageUpdateLogic() }
.onBackpressureBuffer()
.doOnError { LOGGER.error(GlobalVal.DEFAULT, "!-Static Message Service Error-!", it) }
.subscribe()
}
private fun doMessageUpdateLogic(): Mono<Void> {
if (DisCalClient.client == null) return Mono.empty()
return DatabaseManager.getStaticMessagesForShard(Application.getShardCount(), getShardIndex().toInt())
.flatMapMany { Flux.fromIterable(it) }
//We have no interest in updating the message so close to its last update
.filter { Duration.between(Instant.now(), it.lastUpdate).abs().toMinutes() >= 30 }
// Only update messages in range
.filter { Duration.between(Instant.now(), it.scheduledUpdate).toMinutes() <= 60 }
.flatMap { data ->
DisCalClient.client!!.getMessageById(data.channelId, data.messageId).flatMap { message ->
when (data.type) {
StaticMessage.Type.CALENDAR_OVERVIEW -> {
val guildMono = DisCalClient.client!!.getGuildById(data.guildId).cache()
val setMono = guildMono.flatMap(Guild::getSettings)
val calMono = guildMono.flatMap { it.getCalendar(data.calendarNumber) }
Mono.zip(guildMono, setMono, calMono).flatMap(
TupleUtils.function { guild, settings, calendar ->
CalendarEmbed.overview(guild, settings, calendar, true).flatMap {
message.edit(MessageEditSpec.builder()
.embedsOrNull(listOf(it))
.build()
)
}.then(DatabaseManager.updateStaticMessage(data.copy(
lastUpdate = Instant.now(),
scheduledUpdate = data.scheduledUpdate.plus(1, ChronoUnit.DAYS))
))
})
}
}
}.onErrorResume(ClientException.isStatusCode(404)) {
//Message or channel was deleted, delete from database
DatabaseManager.deleteStaticMessage(data.guildId, data.messageId)
}
}.doOnError {
LOGGER.error(GlobalVal.DEFAULT, "Static message update error", it)
}.onErrorResume {
Mono.empty()
}.then()
}
}

View File

@@ -1206,8 +1206,11 @@ object DatabaseManager {
c.createStatement(Queries.INSERT_STATIC_MESSAGE)
.bind(0, message.guildId.asLong())
.bind(1, message.messageId.asLong())
.bind(2, message.type.value)
.bind(3, message.lastUpdate)
.bind(2, message.channelId.asLong())
.bind(3, message.type.value)
.bind(4, message.lastUpdate)
.bind(5, message.scheduledUpdate)
.bind(6, message.calendarNumber)
.execute()
).flatMapMany(Result::getRowsUpdated)
.hasElements()
@@ -1228,10 +1231,13 @@ object DatabaseManager {
.execute()
).flatMapMany { res ->
res.map { row, _ ->
val channelId = Snowflake.of(row["channel_id", Long::class.java]!!)
val type = StaticMessage.Type.valueOf(row["type", Int::class.java]!!)
val lastUpdate = row["last_update", Instant::class.java]!!
val scheduledUpdate = row["scheduled_update", Instant::class.java]!!
val calNum = row["calendar_number", Int::class.java]!!
StaticMessage(guildId, messageId, type, lastUpdate)
StaticMessage(guildId, messageId, channelId, type, lastUpdate, scheduledUpdate, calNum)
}
}.next().retryWhen(Retry.max(3)
.filter(IllegalStateException::class::isInstance)
@@ -1279,7 +1285,38 @@ object DatabaseManager {
}
}
fun getStaticMessagesForShard(shardCount: Int, shardIndex: Int): Mono<List<StaticMessage>> {
return connect { c ->
Mono.from(
c.createStatement(Queries.SELECT_STATIC_MESSAGES_FOR_SHARD)
.bind(0, shardCount)
.bind(1, shardIndex)
.execute()
).flatMapMany { res ->
res.map { row, _ ->
val guildId = Snowflake.of(row["guild_id", Long::class.java]!!)
val messageId = Snowflake.of(row["message_id", Long::class.java]!!)
val channelId = Snowflake.of(row["channel_id", Long::class.java]!!)
val type = StaticMessage.Type.valueOf(row["type", Int::class.java]!!)
val lastUpdate = row["last_update", Instant::class.java]!!
val scheduledUpdate = row["scheduled_update", Instant::class.java]!!
val calNum = row["calendar_number", Int::class.java]!!
StaticMessage(guildId, messageId, channelId, type, lastUpdate, scheduledUpdate, calNum)
}
}.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()
}.collectList()
}
}
/* Event Data */
fun getEventsData(guildId: Snowflake, eventIds: List<String>): Mono<Map<String, EventData>> {
// clean up IDs
val idsToUse = mutableListOf<String>()
@@ -1477,10 +1514,15 @@ private object Queries {
WHERE guild_id = ? AND message_id = ?
""".trimMargin()
@Language("MySQL")
val SELECT_STATIC_MESSAGES_FOR_SHARD = """SELECT * FROM ${Tables.STATIC_MESSAGES}
WHERE MOD(guild_id >> 22, ?) = ?
""".trimMargin()
@Language("MySQL")
val INSERT_STATIC_MESSAGE = """INSERT INTO ${Tables.STATIC_MESSAGES}
(guild_id, message_id, type, last_update)
VALUES(?, ?, ?, ?)
(guild_id, message_id, channel_id, type, last_update, scheduled_update, calendar_number)
VALUES(?, ?, ?, ?, ?, ?, ?)
""".trimMargin()
@Language("MySQL")

View File

@@ -16,10 +16,23 @@ data class StaticMessage(
@SerialName("message_id")
@Serializable(with = SnowflakeAsStringSerializer::class)
val messageId: Snowflake,
@SerialName("channel_id")
@Serializable(with = SnowflakeAsStringSerializer::class)
val channelId: Snowflake,
val type: Type,
@SerialName("last_update")
@Serializable(with = InstantAsStringSerializer::class)
val lastUpdate: Instant,
@SerialName("scheduled_update")
@Serializable(with = InstantAsStringSerializer::class)
val scheduledUpdate: Instant,
@SerialName("calendar_number")
val calendarNumber: Int
) {
enum class Type(val value: Int) {
CALENDAR_OVERVIEW(1);

View File

@@ -0,0 +1,117 @@
{
"name": "displaycal",
"description": "Displays an auto-updating overview of the calendar",
"options": [
{
"name": "time",
"type": 4,
"description": "The hour of the day to update the message (in the calendar's timezone)",
"required": true,
"choices": [
{
"name": "00",
"value": 0
},
{
"name": "01",
"value": 1
},
{
"name": "02",
"value": 2
},
{
"name": "03",
"value": 3
},
{
"name": "04",
"value": 4
},
{
"name": "05",
"value": 5
},
{
"name": "06",
"value": 6
},
{
"name": "07",
"value": 7
},
{
"name": "08",
"value": 8
},
{
"name": "09",
"value": 9
},
{
"name": "10",
"value": 10
},
{
"name": "11",
"value": 11
},
{
"name": "12",
"value": 12
},
{
"name": "13",
"value": 13
},
{
"name": "14",
"value": 14
},
{
"name": "15",
"value": 15
},
{
"name": "16",
"value": 16
},
{
"name": "17",
"value": 17
},
{
"name": "18",
"value": 18
},
{
"name": "19",
"value": 19
},
{
"name": "20",
"value": 20
},
{
"name": "21",
"value": 21
},
{
"name": "22",
"value": 22
},
{
"name": "23",
"value": 23
}
]
},
{
"name": "calendar",
"type": 4,
"description": "The calendar to display. Defaults to 1",
"required": false,
"min_value": 1
}
]
}

View File

@@ -2,8 +2,10 @@ CREATE TABLE IF NOT EXISTS static_messages
(
guild_id BIGINT NOT NULL,
message_id BIGINT NOT NULL,
channel_id BIGINT NOT NULL,
type SMALLINT NOT NULL,
last_update DATETIME NOT NULL,
scheduled_update DATETIME NOT NULL,
PRIMARY KEY (guild_id, message_id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;

View File

@@ -1,5 +1,7 @@
bot.name=DisCal
success.generic=Success
error.unknown=Sorry, an unknown error has occurred.
error.notFound.event=No event with that ID was found. There may be a typo or it may have been deleted.