Implement RSVP waitlist (#127)

* WIP - RSVP waitlist

* waitlist DM embed + logic for filling N > 1 slots.

Handling of single wait listed user and
triggering waitlist in command still needed

* Prevent possible CME

* Remove `@JVMStatic` annotation as no longer needed

* Add rsvp waitlist DMing and triggering

Just need to set up adding to waitlist

* Cleanup some string list stuff

* This should be the last of the work on the waitlist system
This commit is contained in:
Nova Fox
2022-01-16 21:00:13 -06:00
committed by GitHub
parent 0a479d4d97
commit c7406ba27f
12 changed files with 322 additions and 142 deletions
@@ -52,15 +52,26 @@ class RsvpCommand : SlashCommand {
cal.getEvent(eventId).flatMap { calEvent ->
if (!calEvent.isOver()) {
val member = event.interaction.member.get()
calEvent.getRsvp()
.filter { it.hasRoom(member.id.asString()) }
.flatMap { it.removeCompletely(member).thenReturn(it) }
.flatMap { it.addGoingOnTime(member).thenReturn(it) }
.flatMap { calEvent.updateRsvp(it).thenReturn(it) }
.flatMap { RsvpEmbed.list(guild, settings, calEvent, it) }
.flatMap {
event.followupEphemeral(getMessage("onTime.success", settings), it)
}.switchIfEmpty(event.followupEphemeral(getMessage("onTime.failure.limit", settings)))
calEvent.getRsvp().flatMap { rsvp ->
if (rsvp.hasRoom(member.id.asString())) {
rsvp.removeCompletely(member)
.then(rsvp.addGoingOnTime(member))
.then(calEvent.updateRsvp(rsvp))
.then(RsvpEmbed.list(guild, settings, calEvent, rsvp))
.flatMap {
event.followupEphemeral(getMessage("onTime.success", settings), it)
}
} else {
// No room, add to waitlist instead
rsvp.removeCompletely(member)
.doOnNext { rsvp.waitlist.add(member.id.asString()) }
.then(calEvent.updateRsvp(rsvp))
.then(RsvpEmbed.list(guild, settings, calEvent, rsvp))
.flatMap {
event.followupEphemeral(getMessage("onTime.failure.limit", settings), it)
}
}
}
} else {
event.followupEphemeral(getCommonMsg("error.event.ended", settings))
}
@@ -86,15 +97,26 @@ class RsvpCommand : SlashCommand {
cal.getEvent(eventId).flatMap { calEvent ->
if (!calEvent.isOver()) {
val member = event.interaction.member.get()
calEvent.getRsvp()
.filter { it.hasRoom(member.id.asString()) }
.flatMap { it.removeCompletely(member).thenReturn(it) }
.flatMap { it.addGoingLate(member).thenReturn(it) }
.flatMap { calEvent.updateRsvp(it).thenReturn(it) }
.flatMap { RsvpEmbed.list(guild, settings, calEvent, it) }
.flatMap {
event.followupEphemeral(getMessage("late.success", settings), it)
}.switchIfEmpty(event.followupEphemeral(getMessage("late.failure.limit", settings)))
calEvent.getRsvp().flatMap { rsvp ->
if (rsvp.hasRoom(member.id.asString())) {
rsvp.removeCompletely(member)
.then(rsvp.addGoingLate(member))
.then(calEvent.updateRsvp(rsvp))
.then(RsvpEmbed.list(guild, settings, calEvent, rsvp))
.flatMap {
event.followupEphemeral(getMessage("late.success", settings), it)
}
} else {
// No room, add to waitlist instead
rsvp.removeCompletely(member)
.doOnNext { rsvp.waitlist.add(member.id.asString()) }
.then(calEvent.updateRsvp(rsvp))
.then(RsvpEmbed.list(guild, settings, calEvent, rsvp))
.flatMap {
event.followupEphemeral(getMessage("late.failure.limit", settings), it)
}
}
}
} else {
event.followupEphemeral(getCommonMsg("error.event.ended", settings))
}
@@ -187,7 +209,8 @@ class RsvpCommand : SlashCommand {
if (!calEvent.isOver()) {
val member = event.interaction.member.get()
calEvent.getRsvp().flatMap { rsvp ->
rsvp.removeCompletely(member)
// Add next person on waitlist if this user was previously going to attend
rsvp.removeCompletely(member, true)
.then(calEvent.updateRsvp(rsvp))
.then(RsvpEmbed.list(guild, settings, calEvent, rsvp))
.flatMap { event.followupEphemeral(getMessage("remove.success", settings), it) }
@@ -248,6 +271,8 @@ class RsvpCommand : SlashCommand {
if (!calEvent.isOver()) {
calEvent.getRsvp()
.doOnNext { it.limit = limit }
// Handle adding other users to going in the event the limit was increased/removed
.flatMap { it.fillRemaining(guild, settings) }
.flatMap { calEvent.updateRsvp(it).thenReturn(it) }
.flatMap { RsvpEmbed.list(guild, settings, calEvent, it) }
.flatMap {
@@ -283,38 +308,36 @@ class RsvpCommand : SlashCommand {
return@function event.followupEphemeral(getCommonMsg("error.patronOnly", settings))
}
Mono.justOrEmpty(event.interaction.member)
.filterWhen(Member::hasElevatedPermissions)
.flatMap { member ->
guild.getCalendar(calendarNumber).flatMap { cal ->
cal.getEvent(eventId).flatMap { calEvent ->
if (!calEvent.isOver()) {
calEvent.getRsvp().flatMap { rsvp ->
if (role.isEveryone) {
rsvp.clearRole(member.client.rest())
.then(calEvent.updateRsvp(rsvp))
.flatMap { RsvpEmbed.list(guild, settings, calEvent, rsvp) }
.flatMap {
event.followupEphemeral(getMessage("role.success.remove", settings), it)
}
} else {
rsvp.setRole(role)
.then(calEvent.updateRsvp(rsvp))
.then(RsvpEmbed.list(guild, settings, calEvent, rsvp))
.flatMap {
event.followupEphemeral(
getMessage("role.success.set", settings, role.name),
it
)
}
}
Mono.justOrEmpty(event.interaction.member).filterWhen(Member::hasElevatedPermissions).flatMap { member ->
guild.getCalendar(calendarNumber).flatMap { cal ->
cal.getEvent(eventId).flatMap { calEvent ->
if (!calEvent.isOver()) {
calEvent.getRsvp().flatMap { rsvp ->
if (role.isEveryone) {
rsvp.clearRole(member.client.rest())
.then(calEvent.updateRsvp(rsvp))
.flatMap { RsvpEmbed.list(guild, settings, calEvent, rsvp) }
.flatMap {
event.followupEphemeral(getMessage("role.success.remove", settings), it)
}
} else {
rsvp.setRole(role)
.then(calEvent.updateRsvp(rsvp))
.then(RsvpEmbed.list(guild, settings, calEvent, rsvp))
.flatMap {
event.followupEphemeral(
getMessage("role.success.set", settings, role.name),
it
)
}
}
} else {
event.followupEphemeral(getCommonMsg("error.event.ended", settings))
}
}.switchIfEmpty(event.followupEphemeral(getCommonMsg("error.notFound.event", settings)))
}.switchIfEmpty(event.followupEphemeral(getCommonMsg("error.notFound.calendar", settings)))
}.switchIfEmpty(event.followupEphemeral(getCommonMsg("error.perms.elevated", settings)))
} else {
event.followupEphemeral(getCommonMsg("error.event.ended", settings))
}
}.switchIfEmpty(event.followupEphemeral(getCommonMsg("error.notFound.event", settings)))
}.switchIfEmpty(event.followupEphemeral(getCommonMsg("error.notFound.calendar", settings)))
}.switchIfEmpty(event.followupEphemeral(getCommonMsg("error.perms.elevated", settings)))
})
}
}
@@ -47,8 +47,19 @@ object RsvpEmbed : EmbedMaker {
.map(MutableList<String>::asStringList)
.map { it.ifEmpty { "N/a" } }
return Mono.zip(roleMono, onTimeMono, lateMono, undecidedMono, notMono)
.map(TupleUtils.function { role, onTime, late, undecided, notGoing ->
// Wait list users (show up to 3, with (+X) if there are more)
val display = 3
val waitListMono = guild.getMembersFromId(rsvp.waitlist.take(display))
.map(Member::getUsername)
.collectList()
.map { list ->
if (rsvp.waitlist.size > display) "${list.asStringList()} +${rsvp.waitlist.size - display} more"
else if (list.isNotEmpty()) list.asStringList()
else "N/a"
}
return Mono.zip(roleMono, onTimeMono, lateMono, undecidedMono, notMono, waitListMono)
.map(TupleUtils.function { role, onTime, late, undecided, notGoing, waitList ->
val limitValue = if (rsvp.limit < 0) {
getMessage("rsvp", "list.field.limit.value", settings, "${rsvp.getCurrentCount()}")
} else "${rsvp.getCurrentCount()}/${rsvp.limit}"
@@ -63,6 +74,7 @@ object RsvpEmbed : EmbedMaker {
.addField(getMessage("rsvp", "list.field.late", settings), late, false)
.addField(getMessage("rsvp", "list.field.unsure", settings), undecided, false)
.addField(getMessage("rsvp", "list.field.notGoing", settings), notGoing, false)
.addField(getMessage("rsvp", "list.field.waitList", settings), waitList, false)
.footer(getMessage("rsvp", "list.footer", settings), null)
.build()
})
@@ -27,6 +27,7 @@ import org.dreamexposure.discal.core.enums.calendar.CalendarHost
import org.dreamexposure.discal.core.enums.event.EventColor.Companion.fromNameOrHexOrId
import org.dreamexposure.discal.core.enums.time.TimeFormat
import org.dreamexposure.discal.core.extensions.asStringList
import org.dreamexposure.discal.core.extensions.setFromString
import org.dreamexposure.discal.core.logger.LOGGER
import org.dreamexposure.discal.core.utils.GlobalVal.DEFAULT
import org.intellij.lang.annotations.Language
@@ -392,7 +393,7 @@ object DatabaseManager {
if (exists) {
val updateCommand = """UPDATE ${Tables.RSVP} SET
CALENDAR_NUMBER = ?, EVENT_END = ?, GOING_ON_TIME = ?, GOING_LATE = ?,
NOT_GOING = ?, UNDECIDED = ?, RSVP_LIMIT = ?, RSVP_ROLE = ?
NOT_GOING = ?, UNDECIDED = ?, waitlist = ?, RSVP_LIMIT = ?, RSVP_ROLE = ?
WHERE EVENT_ID = ? AND GUILD_ID = ?
""".trimMargin()
@@ -404,14 +405,16 @@ object DatabaseManager {
.bind(3, data.goingLate.asStringList())
.bind(4, data.notGoing.asStringList())
.bind(5, data.undecided.asStringList())
.bind(6, data.limit)
.bind(8, data.eventId)
.bind(9, data.guildId.asString())
.bind(6, data.waitlist.asStringList())
.bind(7, data.limit)
//8 deal with nullable role below
.bind(9, data.eventId)
.bind(10, data.guildId.asString())
).doOnNext { statement ->
if (data.roleId == null)
statement.bindNull(7, Long::class.java)
statement.bindNull(8, Long::class.java)
else
statement.bind(7, data.roleId!!.asString())
statement.bind(8, data.roleId!!.asString())
}.flatMap {
Mono.from(it.execute())
}.flatMapMany(Result::getRowsUpdated)
@@ -420,8 +423,8 @@ object DatabaseManager {
} else if (data.shouldBeSaved()) {
val insertCommand = """INSERT INTO ${Tables.RSVP}
(GUILD_ID, EVENT_ID, CALENDAR_NUMBER, EVENT_END, GOING_ON_TIME, GOING_LATE,
NOT_GOING, UNDECIDED, RSVP_LIMIT, RSVP_ROLE)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
NOT_GOING, UNDECIDED, waitlist, RSVP_LIMIT, RSVP_ROLE)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""".trimMargin()
Mono.just(
@@ -434,12 +437,13 @@ object DatabaseManager {
.bind(5, data.goingLate.asStringList())
.bind(6, data.notGoing.asStringList())
.bind(7, data.undecided.asStringList())
.bind(8, data.limit)
.bind(8, data.waitlist.asStringList())
.bind(9, data.limit)
).doOnNext { statement ->
if (data.roleId == null)
statement.bindNull(9, Long::class.java)
statement.bindNull(10, Long::class.java)
else
statement.bind(9, data.roleId!!.asString())
statement.bind(10, data.roleId!!.asString())
}.flatMap {
Mono.from(it.execute())
}.flatMapMany(Result::getRowsUpdated)
@@ -552,7 +556,7 @@ object DatabaseManager {
lang, prefix, patron, dev, maxCals, branded
)
settings.setDmAnnouncementsString(dmAnnouncementsString)
settings.dmAnnouncements.setFromString(dmAnnouncementsString)
//Store in cache...
DiscalCache.guildSettings[guildId] = settings
@@ -722,10 +726,11 @@ object DatabaseManager {
val data = RsvpData(guildId, eventId, calNumber)
data.eventEnd = row["EVENT_END", Long::class.java]!!
data.setGoingOnTimeFromString(row["GOING_ON_TIME", String::class.java]!!)
data.setGoingLateFromString(row["GOING_LATE", String::class.java]!!)
data.setNotGoingFromString(row["NOT_GOING", String::class.java]!!)
data.setUndecidedFromString(row["UNDECIDED", String::class.java]!!)
data.goingOnTime.setFromString(row["GOING_ON_TIME", String::class.java]!!)
data.goingLate.setFromString(row["GOING_LATE", String::class.java]!!)
data.notGoing.setFromString(row["NOT_GOING", String::class.java]!!)
data.undecided.setFromString(row["UNDECIDED", String::class.java]!!)
data.waitlist.setFromString(row["waitlist", String::class.java]!!)
data.limit = row["RSVP_LIMIT", Int::class.java]!!
//Handle new rsvp role
@@ -755,8 +760,8 @@ object DatabaseManager {
).flatMapMany { res ->
res.map { row, _ ->
val a = Announcement(guildId, announcementId)
a.setSubscriberRoleIdsFromString(row["SUBSCRIBERS_ROLE", String::class.java]!!)
a.setSubscriberUserIdsFromString(row["SUBSCRIBERS_USER", String::class.java]!!)
a.subscriberRoleIds.setFromString(row["SUBSCRIBERS_ROLE", String::class.java]!!)
a.subscriberUserIds.setFromString(row["SUBSCRIBERS_USER", String::class.java]!!)
a.announcementChannelId = row["CHANNEL_ID", String::class.java]!!
a.type = AnnouncementType.valueOf(row["ANNOUNCEMENT_TYPE", String::class.java]!!)
a.modifier = AnnouncementModifier.valueOf(row["MODIFIER", String::class.java]!!)
@@ -791,8 +796,8 @@ object DatabaseManager {
val a = Announcement(guildId, announcementId)
a.calendarNumber = row["CALENDAR_NUMBER", Int::class.java]!!
a.setSubscriberRoleIdsFromString(row["SUBSCRIBERS_ROLE", String::class.java]!!)
a.setSubscriberUserIdsFromString(row["SUBSCRIBERS_USER", String::class.java]!!)
a.subscriberRoleIds.setFromString(row["SUBSCRIBERS_ROLE", String::class.java]!!)
a.subscriberUserIds.setFromString(row["SUBSCRIBERS_USER", String::class.java]!!)
a.announcementChannelId = row["CHANNEL_ID", String::class.java]!!
a.type = AnnouncementType.valueOf(row["ANNOUNCEMENT_TYPE", String::class.java]!!)
a.modifier = AnnouncementModifier.valueOf(row["MODIFIER", String::class.java]!!)
@@ -828,8 +833,8 @@ object DatabaseManager {
val a = Announcement(guildId, announcementId)
a.calendarNumber = row["CALENDAR_NUMBER", Int::class.java]!!
a.setSubscriberRoleIdsFromString(row["SUBSCRIBERS_ROLE", String::class.java]!!)
a.setSubscriberUserIdsFromString(row["SUBSCRIBERS_USER", String::class.java]!!)
a.subscriberRoleIds.setFromString(row["SUBSCRIBERS_ROLE", String::class.java]!!)
a.subscriberUserIds.setFromString(row["SUBSCRIBERS_USER", String::class.java]!!)
a.announcementChannelId = row["CHANNEL_ID", String::class.java]!!
a.type = AnnouncementType.valueOf(row["ANNOUNCEMENT_TYPE", String::class.java]!!)
a.modifier = AnnouncementModifier.valueOf(row["MODIFIER", String::class.java]!!)
@@ -864,8 +869,8 @@ object DatabaseManager {
val a = Announcement(guildId, announcementId)
a.calendarNumber = row["CALENDAR_NUMBER", Int::class.java]!!
a.setSubscriberRoleIdsFromString(row["SUBSCRIBERS_ROLE", String::class.java]!!)
a.setSubscriberUserIdsFromString(row["SUBSCRIBERS_USER", String::class.java]!!)
a.subscriberRoleIds.setFromString(row["SUBSCRIBERS_ROLE", String::class.java]!!)
a.subscriberUserIds.setFromString(row["SUBSCRIBERS_USER", String::class.java]!!)
a.announcementChannelId = row["CHANNEL_ID", String::class.java]!!
a.type = AnnouncementType.valueOf(row["ANNOUNCEMENT_TYPE", String::class.java]!!)
a.modifier = AnnouncementModifier.valueOf(row["MODIFIER", String::class.java]!!)
@@ -901,8 +906,8 @@ object DatabaseManager {
val a = Announcement(guildId, announcementId)
a.calendarNumber = row["CALENDAR_NUMBER", Int::class.java]!!
a.setSubscriberRoleIdsFromString(row["SUBSCRIBERS_ROLE", String::class.java]!!)
a.setSubscriberUserIdsFromString(row["SUBSCRIBERS_USER", String::class.java]!!)
a.subscriberRoleIds.setFromString(row["SUBSCRIBERS_ROLE", String::class.java]!!)
a.subscriberUserIds.setFromString(row["SUBSCRIBERS_USER", String::class.java]!!)
a.announcementChannelId = row["CHANNEL_ID", String::class.java]!!
a.type = AnnouncementType.valueOf(row["ANNOUNCEMENT_TYPE", String::class.java]!!)
a.modifier = AnnouncementModifier.valueOf(row["MODIFIER", String::class.java]!!)
@@ -937,8 +942,8 @@ object DatabaseManager {
val a = Announcement(guildId, announcementId)
a.calendarNumber = row["CALENDAR_NUMBER", Int::class.java]!!
a.setSubscriberRoleIdsFromString(row["SUBSCRIBERS_ROLE", String::class.java]!!)
a.setSubscriberUserIdsFromString(row["SUBSCRIBERS_USER", String::class.java]!!)
a.subscriberRoleIds.setFromString(row["SUBSCRIBERS_ROLE", String::class.java]!!)
a.subscriberUserIds.setFromString(row["SUBSCRIBERS_USER", String::class.java]!!)
a.announcementChannelId = row["CHANNEL_ID", String::class.java]!!
a.type = AnnouncementType.valueOf(row["ANNOUNCEMENT_TYPE", String::class.java]!!)
a.modifier = AnnouncementModifier.valueOf(row["MODIFIER", String::class.java]!!)
@@ -973,8 +978,8 @@ object DatabaseManager {
val a = Announcement(guildId, announcementId)
a.calendarNumber = row["CALENDAR_NUMBER", Int::class.java]!!
a.setSubscriberRoleIdsFromString(row["SUBSCRIBERS_ROLE", String::class.java]!!)
a.setSubscriberUserIdsFromString(row["SUBSCRIBERS_USER", String::class.java]!!)
a.subscriberRoleIds.setFromString(row["SUBSCRIBERS_ROLE", String::class.java]!!)
a.subscriberUserIds.setFromString(row["SUBSCRIBERS_USER", String::class.java]!!)
a.announcementChannelId = row["CHANNEL_ID", String::class.java]!!
a.type = AnnouncementType.valueOf(row["ANNOUNCEMENT_TYPE", String::class.java]!!)
a.modifier = AnnouncementModifier.valueOf(row["MODIFIER", String::class.java]!!)
@@ -1010,8 +1015,8 @@ object DatabaseManager {
val a = Announcement(guildId, announcementId)
a.calendarNumber = row["CALENDAR_NUMBER", Int::class.java]!!
a.setSubscriberRoleIdsFromString(row["SUBSCRIBERS_ROLE", String::class.java]!!)
a.setSubscriberUserIdsFromString(row["SUBSCRIBERS_USER", String::class.java]!!)
a.subscriberRoleIds.setFromString(row["SUBSCRIBERS_ROLE", String::class.java]!!)
a.subscriberUserIds.setFromString(row["SUBSCRIBERS_USER", String::class.java]!!)
a.announcementChannelId = row["CHANNEL_ID", String::class.java]!!
a.type = AnnouncementType.valueOf(row["ANNOUNCEMENT_TYPE", String::class.java]!!)
a.modifier = AnnouncementModifier.valueOf(row["MODIFIER", String::class.java]!!)
@@ -1047,8 +1052,8 @@ object DatabaseManager {
val a = Announcement(guildId, announcementId)
a.calendarNumber = row["CALENDAR_NUMBER", Int::class.java]!!
a.setSubscriberRoleIdsFromString(row["SUBSCRIBERS_ROLE", String::class.java]!!)
a.setSubscriberUserIdsFromString(row["SUBSCRIBERS_USER", String::class.java]!!)
a.subscriberRoleIds.setFromString(row["SUBSCRIBERS_ROLE", String::class.java]!!)
a.subscriberUserIds.setFromString(row["SUBSCRIBERS_USER", String::class.java]!!)
a.announcementChannelId = row["CHANNEL_ID", String::class.java]!!
a.type = AnnouncementType.valueOf(row["ANNOUNCEMENT_TYPE", String::class.java]!!)
a.modifier = AnnouncementModifier.valueOf(row["MODIFIER", String::class.java]!!)
@@ -1480,8 +1485,8 @@ object DatabaseManager {
val a = Announcement(guildId, announcementId)
a.calendarNumber = row["CALENDAR_NUMBER", Int::class.java]!!
a.setSubscriberRoleIdsFromString(row["SUBSCRIBERS_ROLE", String::class.java]!!)
a.setSubscriberUserIdsFromString(row["SUBSCRIBERS_USER", String::class.java]!!)
a.subscriberRoleIds.setFromString(row["SUBSCRIBERS_ROLE", String::class.java]!!)
a.subscriberUserIds.setFromString(row["SUBSCRIBERS_USER", String::class.java]!!)
a.announcementChannelId = row["CHANNEL_ID", String::class.java]!!
a.type = AnnouncementType.valueOf(row["ANNOUNCEMENT_TYPE", String::class.java]!!)
a.modifier = AnnouncementModifier.valueOf(row["MODIFIER", String::class.java]!!)
@@ -20,6 +20,10 @@ fun MutableList<String>.asStringList(): String {
return builder.toString()
}
fun MutableList<String>.setFromString(strList: String) {
this += strList.split(",").filter(String::isNotBlank)
}
fun MutableList<Event>.groupByDate(): Map<ZonedDateTime, List<Event>> {
return this.stream()
@@ -44,10 +44,6 @@ data class GuildSettings(
fun getDmAnnouncementsString() = this.dmAnnouncements.asStringList()
fun setDmAnnouncementsString(dm: String) {
this.dmAnnouncements += dm.split(",").filter(String::isNotBlank)
}
//TODO: Remove when old translation system is dropped
fun getLocale(): Locale {
@@ -12,6 +12,7 @@ import org.dreamexposure.discal.core.enums.announcement.AnnouncementType
import org.dreamexposure.discal.core.enums.event.EventColor
import org.dreamexposure.discal.core.serializers.SnowflakeAsStringSerializer
import org.dreamexposure.discal.core.utils.getEmbedMessage
import java.util.concurrent.CopyOnWriteArrayList
@Serializable
data class Announcement(
@@ -25,10 +26,10 @@ data class Announcement(
override val editing: Boolean = false,
@SerialName("subscriber_roles")
val subscriberRoleIds: MutableList<String> = mutableListOf(),
val subscriberRoleIds: MutableList<String> = CopyOnWriteArrayList(),
@SerialName("subscriber_users")
val subscriberUserIds: MutableList<String> = arrayListOf(),
val subscriberUserIds: MutableList<String> = CopyOnWriteArrayList(),
@SerialName("channel_id")
var announcementChannelId: String = "N/a",
@@ -56,14 +57,6 @@ data class Announcement(
var publish: Boolean = false,
) : Pre(guildId) {
fun setSubscriberRoleIdsFromString(subList: String) {
this.subscriberRoleIds += subList.split(",").filter(String::isNotBlank)
}
fun setSubscriberUserIdsFromString(subList: String) {
this.subscriberUserIds += subList.split(",").filter(String::isNotBlank)
}
fun hasRequiredValues(): Boolean {
return !((this.type == AnnouncementType.SPECIFIC || this.type == AnnouncementType.RECUR) && this.eventId == "N/a")
&& this.announcementChannelId != "N/a"
@@ -2,14 +2,32 @@ package org.dreamexposure.discal.core.`object`.event
import discord4j.common.util.Snowflake
import discord4j.core.DiscordClient
import discord4j.core.`object`.entity.Guild
import discord4j.core.`object`.entity.Member
import discord4j.core.`object`.entity.Role
import discord4j.core.spec.EmbedCreateSpec
import discord4j.discordjson.json.GuildData
import discord4j.discordjson.json.GuildUpdateData
import discord4j.discordjson.json.MessageData
import discord4j.rest.http.client.ClientException
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.dreamexposure.discal.core.`object`.BotSettings
import org.dreamexposure.discal.core.`object`.GuildSettings
import org.dreamexposure.discal.core.entities.Event
import org.dreamexposure.discal.core.enums.time.DiscordTimestampFormat.LONG_DATETIME
import org.dreamexposure.discal.core.extensions.asDiscordTimestamp
import org.dreamexposure.discal.core.extensions.discord4j.getCalendar
import org.dreamexposure.discal.core.extensions.discord4j.getSettings
import org.dreamexposure.discal.core.extensions.embedFieldSafe
import org.dreamexposure.discal.core.extensions.toMarkdown
import org.dreamexposure.discal.core.logger.LOGGER
import org.dreamexposure.discal.core.serializers.SnowflakeAsStringSerializer
import org.dreamexposure.discal.core.utils.GlobalVal
import org.dreamexposure.discal.core.utils.getEmbedMessage
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.function.TupleUtils
import java.util.concurrent.CopyOnWriteArrayList
@Serializable
@@ -46,22 +64,7 @@ data class RsvpData(
val undecided: MutableList<String> = CopyOnWriteArrayList()
//List string stuffs
fun setGoingOnTimeFromString(strList: String) {
this.goingOnTime += strList.split(",").filter(String::isNotBlank)
}
fun setGoingLateFromString(strList: String) {
this.goingLate += strList.split(",").filter(String::isNotBlank)
}
fun setNotGoingFromString(strList: String) {
this.notGoing += strList.split(",").filter(String::isNotBlank)
}
fun setUndecidedFromString(strList: String) {
this.undecided += strList.split(",").filter(String::isNotBlank)
}
val waitlist: MutableList<String> = CopyOnWriteArrayList()
fun getCurrentCount() = this.goingOnTime.size + this.goingLate.size
@@ -71,6 +74,8 @@ data class RsvpData(
else goingOnTime.contains(userId) || goingLate.contains(userId)
}
private fun hasRoom() = limit < 0 || getCurrentCount() + 1 <= limit
fun setRole(id: Snowflake, client: DiscordClient): Mono<Void> {
roleId = id
@@ -108,17 +113,33 @@ data class RsvpData(
}
//Functions
fun removeCompletely(userId: String, client: DiscordClient): Mono<Void> {
fun removeCompletely(userId: String, client: DiscordClient, doWaitlistOp: Boolean = false): Mono<Void> {
// Remove from all lists
goingOnTime.removeAll { userId == it }
goingLate.removeAll { userId == it }
notGoing.removeAll { userId == it }
undecided.removeAll { userId == it }
waitlist.removeAll { userId == it }
return if (roleId != null) removeRole(userId, roleId!!, "Removed RSVP to event with ID $eventId", client)
else Mono.empty()
// Remove role if one is set
val roleMono = if (roleId != null) {
removeRole(userId, roleId!!, "Removed RSVP to event with ID $eventId", client)
} else {
Mono.empty()
}
// If there is now room, add the next waiting user as going
val waitListMono = if (doWaitlistOp && waitlist.isNotEmpty() && hasRoom(waitlist.first())) {
handleWaitListedUser(waitlist.removeFirst(), client)
} else {
Mono.empty()
}
return roleMono.then(waitListMono)
}
fun removeCompletely(member: Member): Mono<Void> = removeCompletely(member.id.asString(), member.client.rest())
fun removeCompletely(member: Member, doWaitlistOp: Boolean = false): Mono<Void> =
removeCompletely(member.id.asString(), member.client.rest(), doWaitlistOp)
fun addGoingOnTime(userId: String, client: DiscordClient): Mono<Void> {
return Mono.just(userId)
@@ -142,15 +163,59 @@ data class RsvpData(
}
}
fun handleWaitListedUser(userId: String, client: DiscordClient): Mono<Void> {
val guild = client.getGuildById(guildId)
val eventMono = guild.getCalendar(calendarNumber).flatMap { it.getEvent(eventId) }
val guildDataMono = guild.data
val settingsMono = guild.getSettings()
val embedMono = Mono.zip(guildDataMono, settingsMono, eventMono).map(
TupleUtils.function { data, settings, event ->
followupEmbed(data, settings, userId, event)
}
)
/* Add the user as attending on time
(it would be rude to show up late if other people want to attend the full event)
*/
return addGoingOnTime(userId, client).then(embedMono).flatMap {
dmUser(userId, it, client)
}.then()
}
fun handleWaitListedUser(member: Member): Mono<Void> = handleWaitListedUser(member.id.asString(), member.client.rest())
fun fillRemaining(guild: Guild, settings: GuildSettings): Mono<RsvpData> {
val eventMono = guild.getCalendar(calendarNumber).flatMap { it.getEvent(eventId) }.cache()
return Flux.fromIterable(waitlist)
.takeWhile { hasRoom() }
.concatMap { userId ->
/* Add the user as attending on time
(it would be rude to show up late if other people want to attend the full event)
*/
addGoingOnTime(userId, guild.client.rest()).then(eventMono).flatMap { event ->
// Send DM
val embed = followupEmbed(guild.data, settings, userId, event)
dmUser(userId, embed, guild.client.rest())
}
}.doOnError {
LOGGER.error(GlobalVal.DEFAULT, "RSVP waitlist processing failed", it)
}.onErrorResume {
Mono.empty()
}.then().thenReturn(this)
}
fun addGoingLate(member: Member): Mono<Void> = addGoingLate(member.id.asString(), member.client.rest())
fun shouldBeSaved(): Boolean {
return this.goingOnTime.isNotEmpty()
|| this.goingLate.isNotEmpty()
|| this.notGoing.isNotEmpty()
|| this.undecided.isNotEmpty()
|| limit != -1
|| roleId != null
|| this.goingLate.isNotEmpty()
|| this.notGoing.isNotEmpty()
|| this.undecided.isNotEmpty()
|| this.waitlist.isNotEmpty()
|| limit != -1
|| roleId != null
}
private fun addRole(userId: String, roleId: Snowflake, reason: String, client: DiscordClient): Mono<Void> {
@@ -164,4 +229,78 @@ data class RsvpData(
.removeMemberRole(Snowflake.of(userId), roleId, reason)
.onErrorResume(ClientException::class.java) { Mono.empty() }
}
private fun followupEmbed(guild: GuildUpdateData, settings: GuildSettings, userId: String, event: Event): EmbedCreateSpec {
val iconUrl = if (guild.icon().isPresent)
"${GlobalVal.discordCdnUrl}/icons/${guild.id().asString()}/${guild.icon().get()}.png"
else GlobalVal.iconUrl
val builder = EmbedCreateSpec.builder()
// Even without branding enabled, we want the user to know what guild this is because it's in DMs
.author(guild.name(), BotSettings.BASE_URL.get(), iconUrl)
.title(getEmbedMessage("rsvp", "waitlist.title", settings))
.description(getEmbedMessage("rsvp", "waitlist.desc", settings, userId, event.name, event.eventId))
.addField(
getEmbedMessage("rsvp", "waitlist.field.start", settings),
event.start.asDiscordTimestamp(LONG_DATETIME),
true
).addField(
getEmbedMessage("rsvp", "waitlist.field.end", settings),
event.end.asDiscordTimestamp(LONG_DATETIME),
true
).footer(getEmbedMessage("rsvp", "waitlist.footer", settings, event.eventId), null)
if (event.location.isNotBlank()) builder.addField(
getEmbedMessage("rsvp", "waitlist.field.location", settings),
event.location.toMarkdown().embedFieldSafe(),
false
)
if (event.image.isNotBlank()) builder.thumbnail(event.image)
return builder.build()
}
private fun followupEmbed(guild: GuildData, settings: GuildSettings, userId: String, event: Event): EmbedCreateSpec {
val iconUrl = if (guild.icon().isPresent)
"${GlobalVal.discordCdnUrl}/icons/${guild.id().asString()}/${guild.icon().get()}.png"
else GlobalVal.iconUrl
val builder = EmbedCreateSpec.builder()
// Even without branding enabled, we want the user to know what guild this is because it's in DMs
.author(guild.name(), BotSettings.BASE_URL.get(), iconUrl)
.title(getEmbedMessage("rsvp", "waitlist.title", settings))
.description(getEmbedMessage("rsvp", "waitlist.desc", settings, userId, event.name, event.eventId))
.addField(
getEmbedMessage("rsvp", "waitlist.field.start", settings),
event.start.asDiscordTimestamp(LONG_DATETIME),
true
).addField(
getEmbedMessage("rsvp", "waitlist.field.end", settings),
event.end.asDiscordTimestamp(LONG_DATETIME),
true
).footer(getEmbedMessage("rsvp", "waitlist.footer", settings, event.eventId), null)
if (event.location.isNotBlank()) builder.addField(
getEmbedMessage("rsvp", "waitlist.field.location", settings),
event.location.toMarkdown().embedFieldSafe(),
false
)
if (event.image.isNotBlank()) builder.thumbnail(event.image)
return builder.build()
}
private fun dmUser(userId: String, embedCreateSpec: EmbedCreateSpec, client: DiscordClient): Mono<MessageData> {
return client.getUserById(Snowflake.of(userId)).privateChannel.flatMap { channelData ->
client.getChannelById(Snowflake.of(channelData.id())).createMessage(embedCreateSpec.asRequest())
}.doOnError {
LOGGER.error("Failed to DM user for RSVP Followup", it)
}.onErrorResume {
Mono.empty()
}
}
}
@@ -11,9 +11,7 @@ import java.time.Duration
import javax.imageio.ImageIO
import javax.imageio.ImageReader
//TODO: Remove jvm static
object ImageValidator {
@JvmStatic
fun validate(url: String, allowGif: Boolean): Mono<Boolean> {
return Mono.fromCallable {
val image = ImageIO.read(URL(url))
@@ -37,3 +37,9 @@ fun getEmbedMessage(embed: String, key: String, settings: GuildSettings, vararg
return src.getMessage(key, args, settings.getLocale())
}
fun getCmdMessage(cmd: String, key: String, settings: GuildSettings, vararg args: String): String {
val src = MessageSourceLoader.getSourceByPath("command/$cmd/$cmd")
return src.getMessage(key, args, settings.getLocale())
}
@@ -0,0 +1,3 @@
ALTER TABLE rsvp
ADD COLUMN waitlist LONGTEXT not null default ''
after UNDECIDED;
@@ -1,19 +1,10 @@
meta.description=A set of commands allowing RSVP functionality
meta.example=/rsvp <subCommand> <eventId> (args...)
meta.description.onTime=RSVPs you as attending the event on time
meta.description.late=RSVPs you as attending the event late
meta.description.notGoing=RSVPs you as not attending the event
meta.description.unsure=RSVPs you as unsure if you will be attending the event
meta.description.remove=Removes your previous RSVP status from the event
meta.description.list=Lists who has RSVPed to the event
meta.description.limit=Sets a limit of how many people may attend the event
meta.description.role=Sets the role assigned when a member RSVPs as attending the event. *Not automatically removed
onTime.success=Confirmed your event attendance as arriving on time.
onTime.failure.limit=Sorry, but the maximum amount of people have RSVPed as attending. Consider using `/rsvp unsure`?
onTime.failure.limit=Sorry, but the maximum amount of people have RSVPed as attending. \n\n\
You have been added to the waitlist and will receive a DM if an opening becomes available.
late.success=Confirmed your event attendance as arriving late.
late.failure.limit=Sorry, but the maximum amount of people have RSVPed as attending. Consider using `/rsvp unsure`?
late.failure.limit=Sorry, but the maximum amount of people have RSVPed as attending. \n\n\
You have been added to the waitlist and will receive a DM if an opening becomes available.
notGoing.success=Confirmed that you will not be able to attend the event.
@@ -24,4 +15,4 @@ remove.success=Successfully removed your response about attending the event.
limit.success=Successfully set the max amount of people that can attend the event to `{0}`!
role.success.set=Members who confirm they will attend the event will receive the `{0}` role!
role.success.remove=Members who confirm they will attend the event will not longer receive a special role.
role.success.remove=Members who confirm they will attend the event will no longer receive a special role.
@@ -7,4 +7,14 @@ list.field.onTime=Arriving On Time
list.field.late=Arriving Late
list.field.unsure=Unsure About Attending
list.field.notGoing=Not Attending
list.field.waitList=Wait List
list.footer=Use `!event view <id>` to view details about this event
waitlist.title=| Event RSVP Followup |
waitlist.desc=<@{0}>, \n\
A space has become available in `{1}`, and you have been moved from the waitlist, to attending. \n\n\
If you can't make the event, please update your RSVP with `/rsvp not-going event:{2}`, otherwise enjoy the event!
waitlist.field.start=Start (Local)
waitlist.field.end=End (Local)
waitlist.field.location=Location
waitlist.footer=Event ID: {0}