Skip to content

Commit

Permalink
feat: slash reminders (#64)
Browse files Browse the repository at this point in the history
* feat: slash reminders

* fix(review): resolve reminder review
  • Loading branch information
ToxicMushroom authored Sep 26, 2023
1 parent 28788bb commit 279032f
Show file tree
Hide file tree
Showing 10 changed files with 424 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ import com.kotlindiscord.kord.extensions.extensions.publicSlashCommand
import com.kotlindiscord.kord.extensions.types.respond
import com.sksamuel.scrimage.ImmutableImage
import me.melijn.apkordex.command.KordExtension
import me.melijn.bot.database.manager.BalanceManager
import me.melijn.bot.database.manager.MissingUserManager
import me.melijn.bot.database.manager.VoiceManager
import me.melijn.bot.database.manager.XPManager
import me.melijn.bot.database.manager.*
import me.melijn.bot.utils.*
import me.melijn.bot.utils.JDAUtil.awaitOrNull
import me.melijn.bot.utils.KordExUtils.atLeast
Expand Down Expand Up @@ -435,8 +432,7 @@ class LevelingExtension : Extension() {
}

private suspend fun getUserNameFor(userId: Long, missing: Boolean): String {
val entryUser = shardManager.takeUnless { missing }?.retrieveUserById(userId)?.awaitOrNull()
if (entryUser == null) missingUserManager.markUserDeleted(userId)
val entryUser = shardManager.takeUnless { missing }?.retrieveUserOrMarkDeleted(userId)
return entryUser?.effectiveName ?: "missing"
}

Expand Down
166 changes: 166 additions & 0 deletions bot/src/main/kotlin/me/melijn/bot/commands/ReminderCommand.kt
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 }
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import dev.minn.jda.ktx.messages.MessageEdit
import me.melijn.apkordex.command.KordExtension
import me.melijn.bot.database.manager.InvitesManager
import me.melijn.bot.database.manager.MemberJoinTrackingManager
import me.melijn.bot.database.manager.MissingUserManager
import me.melijn.bot.events.UserNameListener
import me.melijn.bot.utils.JDAUtil.toHex
import me.melijn.bot.utils.KoinUtil
Expand Down Expand Up @@ -50,6 +51,24 @@ class UtilityExtension : Extension() {
override val name: String = "utility"

override suspend fun setup() {
publicGuildSlashCommand {
name = "cleansing"
description = "Clears the dm blocks, missing member status, delete user status of you"

action {
val missingUsersManager by inject<MissingUserManager>()
val member = member!!
val guildId = member.guild.idLong
val memberId = member.idLong
missingUsersManager.markUserDmsOpen(memberId)
missingUsersManager.markUserReinstated(memberId)
missingUsersManager.markMemberPresent(guildId, memberId)
respond {
content = "Cleansed"
}
}
}

publicGuildSlashCommand(::DevGuildArgs) {
name = "roles"
description = "Show server roles"
Expand Down
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
}
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 }
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ object DeletedUsers : Table("deleted_users") {
override val primaryKey: PrimaryKey = PrimaryKey(userId)
}

@CreateTable
@TableModel(true)
object NoDmsUsers : Table("no_dms_users") {

var userId = long("user_id")

override val primaryKey: PrimaryKey = PrimaryKey(userId)
}

@CreateTable
@TableModel(true)
object MissingMembers : Table("missing_members") {
Expand Down
23 changes: 23 additions & 0 deletions bot/src/main/kotlin/me/melijn/bot/database/model/Reminders.kt
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)
}
}
Loading

0 comments on commit 279032f

Please sign in to comment.