-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: slash reminders * fix(review): resolve reminder review
- Loading branch information
1 parent
28788bb
commit 279032f
Showing
10 changed files
with
424 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
166 changes: 166 additions & 0 deletions
166
bot/src/main/kotlin/me/melijn/bot/commands/ReminderCommand.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
package me.melijn.bot.commands | ||
|
||
import com.kotlindiscord.kord.extensions.commands.Arguments | ||
import com.kotlindiscord.kord.extensions.commands.application.slash.ephemeralSubCommand | ||
import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalDuration | ||
import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalZonedDateTime | ||
import com.kotlindiscord.kord.extensions.commands.converters.impl.string | ||
import com.kotlindiscord.kord.extensions.extensions.Extension | ||
import com.kotlindiscord.kord.extensions.extensions.publicSlashCommand | ||
import com.kotlindiscord.kord.extensions.time.TimestampType | ||
import com.kotlindiscord.kord.extensions.time.toDiscord | ||
import com.kotlindiscord.kord.extensions.types.editingPaginator | ||
import com.kotlindiscord.kord.extensions.types.respond | ||
import kotlinx.datetime.Clock | ||
import me.melijn.apkordex.command.KordExtension | ||
import me.melijn.bot.database.manager.ReminderManager | ||
import me.melijn.bot.services.ReminderService | ||
import me.melijn.bot.utils.KordExUtils.atLeast | ||
import me.melijn.bot.utils.KordExUtils.bail | ||
import me.melijn.bot.utils.KordExUtils.tr | ||
import me.melijn.bot.utils.intRanges | ||
import me.melijn.gen.RemindersData | ||
import me.melijn.kordkommons.utils.escapeCodeBlock | ||
import org.koin.core.component.inject | ||
import java.time.ZonedDateTime | ||
import java.time.temporal.ChronoUnit | ||
import java.util.concurrent.CancellationException | ||
import kotlin.time.Duration.Companion.minutes | ||
|
||
@KordExtension | ||
class ReminderCommand : Extension() { | ||
override val name: String = "reminder" | ||
|
||
val reminderManager by inject<ReminderManager>() | ||
val service by inject<ReminderService>() | ||
|
||
override suspend fun setup() { | ||
publicSlashCommand { | ||
name = "reminder" | ||
description = "Manages reminders" | ||
|
||
ephemeralSubCommand(::ReminderAddArgs) { | ||
name = "add" | ||
description = "Creates a reminder" | ||
|
||
action { | ||
val data = RemindersData( | ||
user.idLong, | ||
Clock.System.now() + arguments.remindAt, | ||
arguments.text | ||
) | ||
reminderManager.store(data) | ||
service.waitingJob.cancel(CancellationException("new reminder state, recheck first")) | ||
|
||
respond { | ||
val timeStamp = data.moment.toDiscord(TimestampType.LongDateTime) | ||
val text = data.reminderText.escapeCodeBlock() | ||
content = tr("reminders.added", timeStamp, text) | ||
} | ||
} | ||
} | ||
|
||
ephemeralSubCommand(::ReminderRemoveArgs) { | ||
name = "remove" | ||
description = "Removes a reminder" | ||
|
||
action { | ||
val reminders = reminderManager.getRemindersSorted(user) | ||
if (reminders.isEmpty()) { | ||
respond { | ||
content = tr("reminders.list.empty") | ||
} | ||
return@action | ||
} | ||
|
||
val toRemove = arguments.indices.list.flatten().mapNotNull { reminders.getOrNull(it - 1) }.toSet() | ||
|
||
reminderManager.bulkDelete(toRemove) | ||
service.waitingJob.cancel(CancellationException("new reminder state, recheck first")) | ||
|
||
respond { | ||
content = tr("reminders.removed", toRemove.size) | ||
} | ||
} | ||
} | ||
|
||
ephemeralSubCommand { | ||
name = "list" | ||
description = "Shows all your reminders" | ||
|
||
action { | ||
val reminders = reminderManager.getRemindersSorted(user) | ||
if (reminders.isEmpty()) { | ||
respond { | ||
content = tr("reminders.list.empty") | ||
} | ||
return@action | ||
} | ||
|
||
val paginator = editingPaginator { | ||
var i = 0 | ||
for (reminderChunk in reminders.chunked(10)) { | ||
this.page { | ||
title = "Reminders" | ||
description = "`id. timestamp reminderText`\n" | ||
for (reminder in reminderChunk) { | ||
val timeStamp = reminder.moment.toDiscord(TimestampType.LongDateTime) | ||
val shortenedText = reminder.reminderText.take(64) | ||
description += "$i. $timeStamp $shortenedText\n" | ||
i++ | ||
} | ||
} | ||
} | ||
} | ||
paginator.send() | ||
} | ||
} | ||
} | ||
} | ||
|
||
internal class ReminderAddArgs : Arguments() { | ||
val text by string { | ||
name = "message" | ||
description = "What the reminder should say" | ||
} | ||
val moment by optionalZonedDateTime { | ||
name = "moment" | ||
description = | ||
"Zoned Date Time (e.g. 2007-12-03T10:15:30+01:00 Europe/Paris) when the reminder should be sent" | ||
validate { | ||
failIf(tr("reminders.tooSoon")) { | ||
this.value?.let { | ||
ChronoUnit.MINUTES.between(it, ZonedDateTime.now()) >= 1 | ||
} ?: false | ||
} | ||
} | ||
} | ||
val duration by optionalDuration { | ||
name = "duration" | ||
description = "The duration between the reminder being sent and now" | ||
validate { | ||
atLeast(name, 1.minutes) | ||
} | ||
} | ||
val remindAt by lazy { | ||
this.moment?.let { | ||
ChronoUnit.MINUTES.between(it, ZonedDateTime.now()).minutes | ||
} ?: this.duration ?: bail("You must supply moment or duration as arguments") | ||
} | ||
} | ||
|
||
inner class ReminderRemoveArgs : Arguments() { | ||
val indices by intRanges { | ||
name = "ids" | ||
description = "id or ids to delete, can be in range format, separate them by comma" | ||
validate { | ||
val countLimit = 100 | ||
failIf(tr("intRanges.providedTooMany", countLimit)) { this.value.list.size > countLimit } | ||
val reminderCount = reminderManager.getByUserIndex(this.context.user.idLong).size | ||
failIf(tr("reminders.outOfBounds", reminderCount)) { | ||
!this.value.list.all { it.first >= 1 && it.last <= reminderCount } | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
56 changes: 53 additions & 3 deletions
56
bot/src/main/kotlin/me/melijn/bot/database/manager/MissingUserManager.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,32 +1,82 @@ | ||
package me.melijn.bot.database.manager | ||
|
||
import me.melijn.ap.injector.Inject | ||
import me.melijn.bot.utils.JDAUtil.awaitOrNull | ||
import me.melijn.bot.utils.KoinUtil.inject | ||
import me.melijn.gen.DeletedUsersData | ||
import me.melijn.gen.MissingMembersData | ||
import me.melijn.gen.NoDmsUsersData | ||
import me.melijn.gen.database.manager.AbstractDeletedUsersManager | ||
import me.melijn.gen.database.manager.AbstractMissingMembersManager | ||
import me.melijn.gen.database.manager.AbstractNoDmsUsersManager | ||
import me.melijn.kordkommons.database.DriverManager | ||
import net.dv8tion.jda.api.entities.User | ||
import net.dv8tion.jda.api.entities.channel.concrete.PrivateChannel | ||
import net.dv8tion.jda.api.sharding.ShardManager | ||
|
||
@Inject | ||
class MissingMemberManager(driverManager: DriverManager) : AbstractMissingMembersManager(driverManager) | ||
|
||
@Inject | ||
class DeletedUserManager(driverManager: DriverManager) : AbstractDeletedUsersManager(driverManager) | ||
|
||
@Inject | ||
class NoDmsUserManager(driverManager: DriverManager) : AbstractNoDmsUsersManager(driverManager) | ||
|
||
@Inject | ||
class MissingUserManager( | ||
private val missingMemberManager: MissingMemberManager, | ||
private val deletedUserManager: DeletedUserManager | ||
) { | ||
private val deletedUserManager: DeletedUserManager, | ||
private val noDmsUserManager: NoDmsUserManager | ||
) { | ||
suspend fun markMemberMissing(guildId: Long, memberId: Long) { | ||
missingMemberManager.store(MissingMembersData(guildId, memberId)) | ||
} | ||
|
||
suspend fun markMemberPresent(guildId: Long, memberId: Long) { | ||
missingMemberManager.deleteById(guildId, memberId) | ||
} | ||
|
||
suspend fun markUserDmsClosed(userId: Long) { | ||
noDmsUserManager.store(NoDmsUsersData(userId)) | ||
} | ||
|
||
suspend fun markUserDmsOpen(userId: Long) { | ||
noDmsUserManager.deleteById(userId) | ||
} | ||
|
||
suspend fun markUserDeleted(userId: Long) { | ||
deletedUserManager.store(DeletedUsersData(userId)) | ||
} | ||
|
||
suspend fun markUserReinstated(userId: Long) { | ||
deletedUserManager.deleteById(userId) | ||
} | ||
} | ||
|
||
suspend fun isDeleted(userId: Long): Boolean = deletedUserManager.getById(userId) != null | ||
suspend fun hasDmsClosed(userId: Long): Boolean = noDmsUserManager.getById(userId) != null | ||
|
||
|
||
} | ||
|
||
suspend fun ShardManager.retrieveUserOrMarkDeleted(userId: Long): User? { | ||
val missingUserManager: MissingUserManager by inject() | ||
val isDeleted = missingUserManager.isDeleted(userId) | ||
if (isDeleted) return null | ||
val user = retrieveUserById(userId).awaitOrNull() | ||
if (user == null) { | ||
missingUserManager.markUserDeleted(userId) | ||
} | ||
return user | ||
} | ||
|
||
suspend fun ShardManager.openPrivateChannelSafely(userId: Long): PrivateChannel? { | ||
val missingUserManager: MissingUserManager by inject() | ||
val hasDmsClosed = missingUserManager.hasDmsClosed(userId) | ||
if (hasDmsClosed) return null | ||
|
||
val user = retrieveUserOrMarkDeleted(userId) ?: return null | ||
val privateChannel = user.openPrivateChannel().awaitOrNull() | ||
if (privateChannel == null) missingUserManager.markUserDmsClosed(userId) | ||
return privateChannel | ||
} |
31 changes: 31 additions & 0 deletions
31
bot/src/main/kotlin/me/melijn/bot/database/manager/ReminderManager.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package me.melijn.bot.database.manager | ||
|
||
import me.melijn.ap.injector.Inject | ||
import me.melijn.bot.database.model.Reminders | ||
import me.melijn.gen.RemindersData | ||
import me.melijn.gen.database.manager.AbstractRemindersManager | ||
import me.melijn.kordkommons.database.DriverManager | ||
import net.dv8tion.jda.api.entities.UserSnowflake | ||
import org.jetbrains.exposed.sql.SortOrder | ||
import org.jetbrains.exposed.sql.selectAll | ||
|
||
@Inject | ||
class ReminderManager(override val driverManager: DriverManager) : AbstractRemindersManager(driverManager) { | ||
|
||
suspend fun getNextUpcomingReminder(): RemindersData? = scopedTransaction { | ||
Reminders.selectAll() | ||
.orderBy(Reminders.moment, SortOrder.ASC) | ||
.limit(1, 0) | ||
.firstOrNull() | ||
?.let { RemindersData.fromResRow(it) } | ||
} | ||
|
||
suspend fun bulkDelete(items: Collection<RemindersData>) = scopedTransaction { | ||
for (item in items) { | ||
delete(item) | ||
} | ||
} | ||
|
||
suspend fun getRemindersSorted(user: UserSnowflake) = getByUserIndex(user.idLong) | ||
.sortedBy { it.moment } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 23 additions & 0 deletions
23
bot/src/main/kotlin/me/melijn/bot/database/model/Reminders.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package me.melijn.bot.database.model | ||
|
||
import me.melijn.apredgres.createtable.CreateTable | ||
import me.melijn.apredgres.tablemodel.TableModel | ||
import org.jetbrains.exposed.sql.Table | ||
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp | ||
|
||
@CreateTable | ||
@TableModel(true) | ||
object Reminders : Table("reminders") { | ||
|
||
val userId = long("user_id") | ||
val moment = timestamp("moment") | ||
val reminderText = text("reminder_text") | ||
|
||
override val primaryKey = PrimaryKey(userId, moment) | ||
|
||
init { | ||
index(false, userId, moment) | ||
index("moment_index", false, moment) | ||
index("user_index", false, userId) | ||
} | ||
} |
Oops, something went wrong.