Add events slash command

This commit is contained in:
NovaFox161
2021-08-06 15:09:02 -04:00
parent 1deec97062
commit 8dfbf29d34
12 changed files with 327 additions and 13 deletions

View File

@@ -0,0 +1,134 @@
package org.dreamexposure.discal.client.commands
import discord4j.core.event.domain.interaction.SlashCommandEvent
import org.dreamexposure.discal.client.message.Responder
import org.dreamexposure.discal.client.message.embed.EventEmbed
import org.dreamexposure.discal.core.`object`.GuildSettings
import org.dreamexposure.discal.core.extensions.discord4j.getCalendar
import org.dreamexposure.discal.core.utils.getCommonMsg
import org.springframework.stereotype.Component
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.function.TupleUtils
import java.time.Instant
@Component
class EventsCommand : SlashCommand {
override val name = "events"
override val ephemeral = false
override fun handle(event: SlashCommandEvent, settings: GuildSettings): Mono<Void> {
return when (event.options[0].name) {
"upcoming" -> upcomingEventsSubcommand(event, settings)
"ongoing" -> ongoingEventsSubcommand(event, settings)
"today" -> eventsTodaySubcommand(event, settings)
else -> Mono.empty() //Never can reach this, makes compiler happy.
}
}
private fun upcomingEventsSubcommand(event: SlashCommandEvent, settings: GuildSettings): Mono<Void> {
//Determine which calendar they want to use...
val calNumMono = Mono.justOrEmpty(event.options[0].getOption("calendar").flatMap { it.value })
.map { it.asLong().toInt() }
.defaultIfEmpty(1)
val amountMono = Mono.justOrEmpty(event.options[0].getOption("amount").flatMap { it.value })
.map { it.asLong().toInt() }
.defaultIfEmpty(1)
return Mono.zip(calNumMono, amountMono).flatMap(TupleUtils.function { calNumb, amount ->
if (amount < 1 || amount > 15) {
Responder.followupEphemeral(event, getMessage("upcoming.failure.outOfRange", settings))
}
event.interaction.guild.flatMap { guild ->
guild.getCalendar(calNumb).flatMap { cal ->
cal.getUpcomingEvents(amount).collectList().flatMap { events ->
if (events.isEmpty()) {
Responder.followup(event, getMessage("upcoming.success.none", settings))
} else if (events.size == 1) {
Responder.followup(
event,
getMessage("upcoming.success.one", settings),
EventEmbed.getFull(guild, settings, events[0])
)
} else {
Responder.followup(event, getMessage("upcoming.success.many", settings, "${events.size}"))
.flatMapMany {
Flux.fromIterable(events)
}.flatMap {
Responder.followup(event, EventEmbed.getCondensed(guild, settings, it))
}.then()
}
}
}.switchIfEmpty(Responder.followupEphemeral(event, getCommonMsg("error.notFound.calendar", settings)))
}
}).then()
}
private fun ongoingEventsSubcommand(event: SlashCommandEvent, settings: GuildSettings): Mono<Void> {
return Mono.justOrEmpty(event.options[0].getOption("calendar").flatMap { it.value })
.map { it.asLong().toInt() }
.defaultIfEmpty(1).flatMap { calNum ->
event.interaction.guild.flatMap { guild ->
guild.getCalendar(calNum).flatMap { cal ->
cal.getOngoingEvents().collectList().flatMap { events ->
if (events.isEmpty()) {
Responder.followupEphemeral(
event,
getMessage("ongoing.success.none", settings)
)
} else if (events.size == 1) {
Responder.followup(
event,
getMessage("ongoing.success.one", settings),
EventEmbed.getFull(guild, settings, events[0])
)
} else {
Responder.followup(event,
getMessage("ongoing.success.many", settings, "${events.size}")
).flatMapMany {
Flux.fromIterable(events)
}.flatMap {
Responder.followup(event, EventEmbed.getCondensed(guild, settings, it))
}.then()
}
}
}
}
}.then()
}
private fun eventsTodaySubcommand(event: SlashCommandEvent, settings: GuildSettings): Mono<Void> {
return Mono.justOrEmpty(event.options[0].getOption("calendar").flatMap { it.value })
.map { it.asLong().toInt() }
.defaultIfEmpty(1).flatMap { calNum ->
event.interaction.guild.flatMap { guild ->
guild.getCalendar(calNum).flatMap { cal ->
cal.getEventsInNext24HourPeriod(Instant.now()).collectList().flatMap { events ->
if (events.isEmpty()) {
Responder.followupEphemeral(
event,
getMessage("today.success.none", settings)
)
} else if (events.size == 1) {
Responder.followup(
event,
getMessage("today.success.one", settings),
EventEmbed.getFull(guild, settings, events[0])
)
} else {
Responder.followup(event,
getMessage("today.success.many", settings, "${events.size}")
).flatMapMany {
Flux.fromIterable(events)
}.flatMap {
Responder.followup(event, EventEmbed.getCondensed(guild, settings, it))
}.then()
}
}
}
}
}.then()
}
}

View File

@@ -24,6 +24,15 @@ object Responder {
return sendFollowup(event, spec)
}
fun followup(event: InteractionCreateEvent, message: String, embed: EmbedCreateSpec): Mono<MessageData> {
val spec = WebhookExecuteRequest.builder()
.content(message)
.addEmbed(embed.asRequest())
.build()
return sendFollowup(event, spec)
}
fun followupEphemeral(event: InteractionCreateEvent, embed: EmbedCreateSpec): Mono<MessageData> {
val spec = WebhookExecuteRequest.builder()
.addEmbed(embed.asRequest())
@@ -40,6 +49,14 @@ object Responder {
return sendFollowupEphemeral(event, spec)
}
fun followupEphemeral(event: InteractionCreateEvent, message: String, embed: EmbedCreateSpec): Mono<MessageData> {
val spec = WebhookExecuteRequest.builder()
.content(message)
.addEmbed(embed.asRequest())
.build()
return sendFollowupEphemeral(event, spec)
}
private fun sendFollowup(event: InteractionCreateEvent, request: WebhookExecuteRequest) =
event.interactionResponse.createFollowupMessage(MultipartRequest.ofRequest(request))

View File

@@ -0,0 +1,57 @@
package org.dreamexposure.discal.client.message.embed
import discord4j.core.`object`.entity.Guild
import discord4j.core.spec.EmbedCreateSpec
import org.dreamexposure.discal.core.`object`.GuildSettings
import org.dreamexposure.discal.core.entities.Event
import java.time.Instant
object EventEmbed : EmbedMaker {
fun getFull(guild: Guild, settings: GuildSettings, event: Event): EmbedCreateSpec {
val builder = defaultBuilder(guild, settings)
.title(getMessage("event", "full.title", settings))
.footer(getMessage("event", "full.footer", settings, event.eventId), null)
.color(event.color.asColor())
if (event.name.isNotEmpty())
builder.addField(getMessage("event", "full.field.name", settings), event.name, false)
if (event.description.isNotEmpty())
builder.addField(getMessage("event", "full.field.desc", settings), event.description, false)
builder.addField(getMessage("event", "full.field.start", settings), timestamp(event.start), true)
builder.addField(getMessage("event", "full.field.end", settings), timestamp(event.end), true)
if (event.location.isNotEmpty())
builder.addField(getMessage("event", "full.field.location", settings), event.location, false)
builder.addField(getMessage("event", "full.field.cal", settings), "${event.calendar.calendarNumber}", false)
if (event.image.isNotEmpty())
builder.image(event.image)
return builder.build()
}
fun getCondensed(guild: Guild, settings: GuildSettings, event: Event): EmbedCreateSpec {
val builder = defaultBuilder(guild, settings)
.title(getMessage("event", "con.title", settings))
.footer(getMessage("event", "con.footer", settings, event.eventId), null)
.color(event.color.asColor())
if (event.name.isNotEmpty())
builder.addField(getMessage("event", "con.field.name", settings), event.name, false)
builder.addField(getMessage("event", "con.field.start", settings), timestamp(event.start), true)
if (event.location.isNotEmpty())
builder.addField(getMessage("event", "con.field.location", settings), event.location, false)
if (event.image.isNotEmpty())
builder.thumbnail(event.image)
return builder.build()
}
private fun timestamp(time: Instant): String = "<t:${time.toEpochMilli() / 1000}:F>"
}

View File

@@ -110,6 +110,15 @@ interface Calendar {
*/
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]

View File

@@ -57,6 +57,11 @@ interface Event {
*/
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.
@@ -74,7 +79,7 @@ interface Event {
val end: Instant
/**
* Whether or not the event is a recurring event.
* Whether the event is a recurring event.
*/
val recur: Boolean

View File

@@ -77,6 +77,17 @@ class GoogleCalendar internal constructor(
return GoogleEvent.get(this, eventId)
}
override fun getUpcomingEvents(amount: Int): Flux<Event> {
return EventWrapper.getEvents(calendarData, amount, System.currentTimeMillis())
.flatMapMany { Flux.fromIterable(it) }
.flatMap { event ->
DatabaseManager.getEventData(guildId, event.id)
.map {
GoogleEvent(this, it, event)
}
}
}
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

View File

@@ -33,6 +33,9 @@ class GoogleEvent internal constructor(
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())

View File

@@ -68,7 +68,7 @@ object EventWrapper {
}.onErrorResume { Mono.empty() } //Can safely ignore this, the event just doesn't exist.
}
fun getEvents(calData: CalendarData, amount: Int, start: Long): Mono<MutableList<Event>> {
fun getEvents(calData: CalendarData, amount: Int, start: Long): Mono<List<Event>> {
return GoogleAuthWrapper.getCalendarService(calData).flatMap { service: Calendar ->
Mono.fromCallable {
service.events()

View File

@@ -0,0 +1,55 @@
{
"name": "events",
"description": "Lists events",
"options": [
{
"name": "upcoming",
"type": 1,
"description": "Lists the next X upcoming events",
"required": false,
"options": [
{
"name": "amount",
"type": 4,
"description": "The amount of upcoming events to see. range: 1-15. Defaults to 1",
"required": false
},
{
"name": "calendar",
"type": 4,
"description": "The calendar to pull the events from. Defaults to 1",
"required": false
}
]
},
{
"name": "ongoing",
"type": 1,
"description": "Lists the currently ongoing events",
"required": false,
"options": [
{
"name": "calendar",
"type": 4,
"description": "The calendar to pull the events from. Defaults to 1",
"required": false
}
]
},
{
"name": "today",
"type": 1,
"description": "Lists the events occurring in the next 24 hours",
"required": false,
"options": [
{
"name": "calendar",
"type": 4,
"description": "The calendar to pull the events from. Defaults to 1",
"required": false
}
]
}
],
"default_permissions": true
}

View File

@@ -1,18 +1,15 @@
meta.description=Looks up and lists events on the calendar
meta.example=/events (args...)
meta.description.search=Looks up and lists calendar events by keyword
meta.description.upcoming=Lists the next X upcoming events.
meta.description.today=Lists calendar events upcoming in the next 24 hours
meta.description.ongoing=Lists calendar events that are currently happening
simple.success.none=No upcoming events were found.
simple.success.one=1 upcoming event found.
simple.success.many={0} upcoming events found. There may be a delay in listing the events...
simple.failure.tooLow=The amount provided must be greater than 0.
simple.failure.tooHigh=The amount provided must be less than 16.
day.success.none=No upcoming events were found in the next 24 hours.
day.success.one=1 upcoming event was found in the next 24 hours.
day.success.many={0} upcoming events found in the next 24 hours. There may be a delay in listing the events...
day.failure.badArgs=Invalid arguments were supplied. See command info with `/help events day`
upcoming.success.none=No upcoming events were found.
upcoming.success.one=1 upcoming event found.
upcoming.success.many={0} upcoming events found. There may be a delay in listing the events...
upcoming.failure.outOfRange=The amount provided must be between 1 and 15.
today.success.none=No upcoming events were found in the next 24 hours.
today.success.one=1 upcoming event was found in the next 24 hours.
today.success.many={0} upcoming events found in the next 24 hours. There may be a delay in listing the events...
ongoing.success.none=No ongoing events were found.
ongoing.success.one=1 ongoing event was found.
ongoing.success.many={0} ongoing events found
ongoing.failure.badArgs=Invalid arguments were supplied. See command info with `/help events ongoing`

View File

@@ -0,0 +1,14 @@
full.title=Event Info
full.field.name=Name
full.field.desc=Description
full.field.start=Event Start
full.field.end=Event End
full.field.location=Location
full.field.cal=Calendar
full.footer=Event ID: {0}
con.title=Condensed Event Info
con.field.name=Name
con.field.start=Start
con.field.location=Location
con.footer=Event ID: {0}

View File

@@ -6,6 +6,8 @@ import discord4j.common.JacksonResources
import discord4j.discordjson.json.ApplicationCommandData
import discord4j.discordjson.json.ApplicationCommandRequest
import discord4j.rest.RestClient
import org.dreamexposure.discal.core.logger.LOGGER
import org.dreamexposure.discal.core.utils.GlobalVal.DEFAULT
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
@@ -27,12 +29,17 @@ class GlobalCommandRegistrar(
.collectMap(ApplicationCommandData::name)
.block()!!
var added = 0
var removed = 0
var updated = 0
val commands = mutableMapOf<String, ApplicationCommandRequest>()
for (res in matcher.getResources("commands/*.json")) {
val request = d4jMapper.objectMapper.readValue<ApplicationCommandRequest>(res.inputStream)
commands[request.name()] = request
if (discordCommands[request.name()] == null) {
added++
applicationService.createGlobalApplicationCommand(applicationId, request).block()
}
}
@@ -41,6 +48,7 @@ class GlobalCommandRegistrar(
val discordCommandId = discordCommand.id().toLong()
val command = commands[discordCommandName]
if (command == null) { // Removed command.json, delete global command
removed++
applicationService.deleteGlobalApplicationCommand(applicationId, discordCommandId).block()
continue
}
@@ -50,8 +58,12 @@ class GlobalCommandRegistrar(
|| discordCommand.defaultPermission() != command.defaultPermission()
if (changed) {
updated++
applicationService.modifyGlobalApplicationCommand(applicationId, discordCommandId, command).block()
}
}
//Send log message with details on changes...
LOGGER.info(DEFAULT, "Slash commands: $added Added | $updated Updated | $removed Removed")
}
}