mirror of
https://github.com/DreamExposure/DisCal-Discord-Bot.git
synced 2026-01-23 12:29:31 -06:00
Add events slash command
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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>"
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
|
||||
55
core/src/main/resources/commands/events.json
Normal file
55
core/src/main/resources/commands/events.json
Normal 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
|
||||
}
|
||||
@@ -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`
|
||||
|
||||
14
core/src/main/resources/i18n/embed/event.properties
Normal file
14
core/src/main/resources/i18n/embed/event.properties
Normal 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}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user