Adds all day and multi-day event displays to /displaycal (#128)

* Working on doing multi-day and all events in displaycal

* Fixing more stuff for this feature

* Properly handle 3+ day events
This commit is contained in:
Nova Fox
2022-01-22 21:49:33 -06:00
committed by GitHub
parent caddd4eb98
commit 2c9e16330c
5 changed files with 134 additions and 33 deletions
@@ -15,6 +15,7 @@ import reactor.core.publisher.Mono
import java.time.Instant
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
object CalendarEmbed : EmbedMaker {
fun link(guild: Guild, settings: GuildSettings, calNumber: Int, overview: Boolean): Mono<EmbedCreateSpec> {
@@ -43,7 +44,7 @@ object CalendarEmbed : EmbedMaker {
}
fun overview(guild: Guild, settings: GuildSettings, calendar: Calendar, showUpdate: Boolean): Mono<EmbedCreateSpec> {
return calendar.getUpcomingEvents(15).collectList().map { it.groupByDate() }.map { events ->
return calendar.getUpcomingEvents(15).collectList().map { it.groupByDateMulti() }.map { events ->
val builder = defaultBuilder(guild, settings)
//Handle optional fields
@@ -62,10 +63,31 @@ object CalendarEmbed : EmbedMaker {
val content = StringBuilder().append("```\n")
sortedEvents.forEach {
content.append(it.start.humanReadableTime(it.timezone, settings.timeFormat))
.append(" - ")
.append(it.end.humanReadableTime(it.timezone, settings.timeFormat))
.append(" | ")
// determine time length
val timeDisplayLen = ("${it.start.humanReadableTime(it.timezone, settings.timeFormat)} -" +
" ${it.end.humanReadableTime(it.timezone,settings.timeFormat)} ").length
// Displaying time
if (it.isAllDay()) {
content.append(getCommonMsg("generic.time.allDay", settings).padCenter(timeDisplayLen))
.append("| ")
} else {
// Add start text
var str = if (it.start.isBefore(date.key.toInstant())) {
"${getCommonMsg("generic.time.continued", settings)} - "
} else {
"${it.start.humanReadableTime(it.timezone, settings.timeFormat)} - "
}
// Add end text
str += if (it.end.isAfter(date.key.toInstant().plus(1, ChronoUnit.DAYS))) {
getCommonMsg("generic.time.continued", settings)
} else {
"${it.end.humanReadableTime(it.timezone, settings.timeFormat)} "
}
content.append(str.padCenter(timeDisplayLen))
.append("| ")
}
// Display name or ID if not set
if (it.name.isNotBlank()) content.append(it.name)
else content.append(getMessage("calendar", "link.field.id", settings)).append(" ${it.eventId}")
content.append("\n")
@@ -16,6 +16,7 @@ import org.dreamexposure.discal.core.utils.GlobalVal.JSON_FORMAT
import org.json.JSONObject
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.time.Duration
import java.time.Instant
import java.time.ZoneId
@@ -111,15 +112,15 @@ interface Event {
*/
fun getLinkedAnnouncements(): Flux<Announcement> {
return DatabaseManager.getAnnouncements(this.guildId)
.flatMapMany { Flux.fromIterable(it) }
.filter { ann ->
when (ann.type) {
AnnouncementType.UNIVERSAL -> return@filter true
AnnouncementType.COLOR -> return@filter ann.eventColor == this.color
AnnouncementType.SPECIFIC -> return@filter ann.eventId == this.eventId
AnnouncementType.RECUR -> return@filter this.eventId.contains(ann.eventId)
}
.flatMapMany { Flux.fromIterable(it) }
.filter { ann ->
when (ann.type) {
AnnouncementType.UNIVERSAL -> return@filter true
AnnouncementType.COLOR -> return@filter ann.eventColor == this.color
AnnouncementType.SPECIFIC -> return@filter ann.eventId == this.eventId
AnnouncementType.RECUR -> return@filter this.eventId.contains(ann.eventId)
}
}
}
/**
@@ -155,24 +156,40 @@ interface Event {
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()
}
//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)
.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,6 +1,8 @@
package org.dreamexposure.discal.core.extensions
import org.dreamexposure.discal.core.entities.Event
import org.dreamexposure.discal.core.logger.LOGGER
import java.time.Instant
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
import java.time.temporal.TemporalAdjusters
@@ -27,9 +29,45 @@ fun MutableList<String>.setFromString(strList: String) {
fun MutableList<Event>.groupByDate(): Map<ZonedDateTime, List<Event>> {
return this.stream()
.collect(Collectors.groupingBy {
ZonedDateTime.ofInstant(it.start, it.timezone).truncatedTo(ChronoUnit.DAYS)
.with(TemporalAdjusters.ofDateAdjuster { identity -> identity })
}).toSortedMap()
.collect(Collectors.groupingBy {
ZonedDateTime.ofInstant(it.start, it.timezone).truncatedTo(ChronoUnit.DAYS)
.with(TemporalAdjusters.ofDateAdjuster { identity -> identity })
}).toSortedMap()
}
// TODO: This could use some optimization, but we'll leave it for now
fun MutableList<Event>.groupByDateMulti(): Map<ZonedDateTime, List<Event>> {
// Each of the days events start on, their ending is ignored
val dates = this.map {
ZonedDateTime.ofInstant(it.start, it.timezone).truncatedTo(ChronoUnit.DAYS)
.with(TemporalAdjusters.ofDateAdjuster { identity -> identity })
}.distinct().sorted()
val multi: MutableMap<ZonedDateTime, List<Event>> = mutableMapOf()
dates.forEach {
LOGGER.debug("Date: $it")
val range = LongRange(it.toEpochSecond(), it.plusHours(23).plusMinutes(59).toEpochSecond())
LOGGER.debug("Range: ${Instant.ofEpochSecond(range.first)} - ${Instant.ofEpochSecond(range.last)}")
val events: MutableList<Event> = mutableListOf()
this.forEach { event ->
// When we check event end, we bump it back a second in order to prevent weirdness.
if (range.contains(event.start.epochSecond) || range.contains(event.end.epochSecond - 1)) {
LOGGER.debug("Event in range? Start: ${event.start} | End: ${event.end} | Name: ${event.name}")
events.add(event)
} else if (event.start.epochSecond < range.first && event.end.epochSecond > range.last) {
// This is a multi-day event that starts before today and ends after today
LOGGER.debug("Event extends beyond range? Start: ${event.start} | End: ${event.end} | Name: ${event.name}")
events.add(event)
}
}
LOGGER.debug("events in this range: ${events.size}")
multi[it] = events
}
return multi.toSortedMap()
}
@@ -29,3 +29,24 @@ fun String.isValidTimezone(): Boolean {
}
fun String.isValidImage(allowGif: Boolean) = ImageValidator.validate(this, allowGif)
fun String.padCenter(length: Int, padChar: Char = ' '): String {
if (this.length >= length) return this
val chars: CharArray = this.toCharArray()
val delta = length - chars.size
val a = if (delta % 2 == 0) delta / 2 else delta / 2 + 1
val b = a + chars.size
val output = CharArray(length)
for (i in 0 until length) {
if (i < a) {
output[i] = padChar
} else if (i < b) {
output[i] = chars[i - a]
} else {
output[i] = padChar
}
}
return String(output)
}
@@ -2,6 +2,9 @@ bot.name=DisCal
success.generic=Success
generic.time.allDay=All day
generic.time.continued=Cont.
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.