From 8dfbf29d34d2c3058009bfbb6eff0e1c70d73fe9 Mon Sep 17 00:00:00 2001 From: NovaFox161 Date: Fri, 6 Aug 2021 15:09:02 -0400 Subject: [PATCH] Add events slash command --- .../discal/client/commands/EventsCommand.kt | 134 ++++++++++++++++++ .../discal/client/message/Responder.kt | 17 +++ .../discal/client/message/embed/EventEmbed.kt | 57 ++++++++ .../discal/core/entities/Calendar.kt | 9 ++ .../discal/core/entities/Event.kt | 7 +- .../core/entities/google/GoogleCalendar.kt | 11 ++ .../core/entities/google/GoogleEvent.kt | 3 + .../core/wrapper/google/EventWrapper.kt | 2 +- core/src/main/resources/commands/events.json | 55 +++++++ .../i18n/command/events/events.properties | 19 ++- .../resources/i18n/embed/event.properties | 14 ++ .../network/discord/GlobalCommandRegistrar.kt | 12 ++ 12 files changed, 327 insertions(+), 13 deletions(-) create mode 100644 client/src/main/kotlin/org/dreamexposure/discal/client/commands/EventsCommand.kt create mode 100644 client/src/main/kotlin/org/dreamexposure/discal/client/message/embed/EventEmbed.kt create mode 100644 core/src/main/resources/commands/events.json create mode 100644 core/src/main/resources/i18n/embed/event.properties diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/commands/EventsCommand.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/EventsCommand.kt new file mode 100644 index 00000000..1e93118a --- /dev/null +++ b/client/src/main/kotlin/org/dreamexposure/discal/client/commands/EventsCommand.kt @@ -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 { + 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 { + //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 { + 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 { + 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() + } +} diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/message/Responder.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/message/Responder.kt index e0629e6d..55c61a7a 100644 --- a/client/src/main/kotlin/org/dreamexposure/discal/client/message/Responder.kt +++ b/client/src/main/kotlin/org/dreamexposure/discal/client/message/Responder.kt @@ -24,6 +24,15 @@ object Responder { return sendFollowup(event, spec) } + fun followup(event: InteractionCreateEvent, message: String, embed: EmbedCreateSpec): Mono { + val spec = WebhookExecuteRequest.builder() + .content(message) + .addEmbed(embed.asRequest()) + .build() + + return sendFollowup(event, spec) + } + fun followupEphemeral(event: InteractionCreateEvent, embed: EmbedCreateSpec): Mono { 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 { + 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)) diff --git a/client/src/main/kotlin/org/dreamexposure/discal/client/message/embed/EventEmbed.kt b/client/src/main/kotlin/org/dreamexposure/discal/client/message/embed/EventEmbed.kt new file mode 100644 index 00000000..23ad506e --- /dev/null +++ b/client/src/main/kotlin/org/dreamexposure/discal/client/message/embed/EventEmbed.kt @@ -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 = "" +} diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/entities/Calendar.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/entities/Calendar.kt index 500eca0f..20a157be 100644 --- a/core/src/main/kotlin/org/dreamexposure/discal/core/entities/Calendar.kt +++ b/core/src/main/kotlin/org/dreamexposure/discal/core/entities/Calendar.kt @@ -110,6 +110,15 @@ interface Calendar { */ fun getEvent(eventId: String): Mono + /** + * 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 + /** * Requests to retrieve all ongoing [events][Event] (starting no more than 2 weeks ago). * If an error occurs, it is emitted through the [Flux] diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/entities/Event.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/entities/Event.kt index b687cfb1..3b76acd1 100644 --- a/core/src/main/kotlin/org/dreamexposure/discal/core/entities/Event.kt +++ b/core/src/main/kotlin/org/dreamexposure/discal/core/entities/Event.kt @@ -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 diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/entities/google/GoogleCalendar.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/entities/google/GoogleCalendar.kt index 1c6e553d..1cef0237 100644 --- a/core/src/main/kotlin/org/dreamexposure/discal/core/entities/google/GoogleCalendar.kt +++ b/core/src/main/kotlin/org/dreamexposure/discal/core/entities/google/GoogleCalendar.kt @@ -77,6 +77,17 @@ class GoogleCalendar internal constructor( return GoogleEvent.get(this, eventId) } + override fun getUpcomingEvents(amount: Int): Flux { + 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 { val start = System.currentTimeMillis() - Duration.ofDays(14).toMillis() // 2 weeks ago val end = System.currentTimeMillis() + Duration.ofDays(1).toMillis() // One day from now diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/entities/google/GoogleEvent.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/entities/google/GoogleEvent.kt index 492041d3..89dbc6d0 100644 --- a/core/src/main/kotlin/org/dreamexposure/discal/core/entities/google/GoogleEvent.kt +++ b/core/src/main/kotlin/org/dreamexposure/discal/core/entities/google/GoogleEvent.kt @@ -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()) diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/wrapper/google/EventWrapper.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/wrapper/google/EventWrapper.kt index fa72db64..64676039 100644 --- a/core/src/main/kotlin/org/dreamexposure/discal/core/wrapper/google/EventWrapper.kt +++ b/core/src/main/kotlin/org/dreamexposure/discal/core/wrapper/google/EventWrapper.kt @@ -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> { + fun getEvents(calData: CalendarData, amount: Int, start: Long): Mono> { return GoogleAuthWrapper.getCalendarService(calData).flatMap { service: Calendar -> Mono.fromCallable { service.events() diff --git a/core/src/main/resources/commands/events.json b/core/src/main/resources/commands/events.json new file mode 100644 index 00000000..4ded4bec --- /dev/null +++ b/core/src/main/resources/commands/events.json @@ -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 +} diff --git a/core/src/main/resources/i18n/command/events/events.properties b/core/src/main/resources/i18n/command/events/events.properties index 45d494d5..60e1ace3 100644 --- a/core/src/main/resources/i18n/command/events/events.properties +++ b/core/src/main/resources/i18n/command/events/events.properties @@ -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` diff --git a/core/src/main/resources/i18n/embed/event.properties b/core/src/main/resources/i18n/embed/event.properties new file mode 100644 index 00000000..3c7fd8c3 --- /dev/null +++ b/core/src/main/resources/i18n/embed/event.properties @@ -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} diff --git a/server/src/main/kotlin/org/dreamexposure/discal/server/network/discord/GlobalCommandRegistrar.kt b/server/src/main/kotlin/org/dreamexposure/discal/server/network/discord/GlobalCommandRegistrar.kt index 312fa38e..7e314c22 100644 --- a/server/src/main/kotlin/org/dreamexposure/discal/server/network/discord/GlobalCommandRegistrar.kt +++ b/server/src/main/kotlin/org/dreamexposure/discal/server/network/discord/GlobalCommandRegistrar.kt @@ -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() for (res in matcher.getResources("commands/*.json")) { val request = d4jMapper.objectMapper.readValue(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") } }