Skip to content

Commit

Permalink
Get slash commands working in threads (kinda...)
Browse files Browse the repository at this point in the history
  • Loading branch information
ScoreUnder committed Oct 22, 2021
1 parent 072976c commit d799598
Show file tree
Hide file tree
Showing 9 changed files with 71 additions and 47 deletions.
36 changes: 24 additions & 12 deletions src/main/scala/score/discord/canti/command/FindCommand.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package score.discord.canti.command
import cps.*
import cps.monads.FutureAsyncMonad
import com.google.re2j.{Pattern as RE2JPattern, PatternSyntaxException}
import net.dv8tion.jda.api.entities.{GuildChannel, Message, MessageChannel}
import net.dv8tion.jda.api.entities.{Guild, GuildChannel, Message, MessageChannel}
import net.dv8tion.jda.api.events.GenericEvent
import net.dv8tion.jda.api.events.interaction.ButtonClickEvent
import net.dv8tion.jda.api.hooks.EventListener
Expand Down Expand Up @@ -35,6 +35,8 @@ import scala.util.chaining.scalaUtilChainingOps

class FindCommand(using messageOwnership: MessageOwnership, replyCache: ReplyCache)
extends GenericCommand:
private val logger = loggerOf[FindCommand]

override def name: String = "find"

override val aliases: Seq[String] = List("id")
Expand Down Expand Up @@ -62,16 +64,21 @@ class FindCommand(using messageOwnership: MessageOwnership, replyCache: ReplyCac

override def execute(ctx: CommandInvocation): Future[RetrievableMessage] =
async {
val reply = makeSearchReply(ctx.invoker.channel, ctx.args(arg))
val guild = ctx.invoker.member.toOption.map(_.getGuild)
val reply = makeSearchReply(ctx.invoker.channel, guild, ctx.args(arg))
await(ctx.invoker.reply(reply))
}

private def makeSearchReply(channel: MessageChannel, searchTerm: String): Message =
private def makeSearchReply(
channel: Option[MessageChannel],
guild: Option[Guild],
searchTerm: String
): Message =
val maxResults = 10
val searchTermSanitised = MessageUtils.sanitiseCode(searchTerm)
Try(RE2JPattern.compile(searchTerm, RE2JPattern.CASE_INSENSITIVE).nn)
.map { searchPattern =>
val results = getSearchResults(channel, searchPattern)
val results = getSearchResults(channel, guild, searchPattern)
.take(maxResults + 1)
.zip(ReactListener.ICONS.iterator ++ Iterator.continually(""))
.map { case ((msg, id), icon) => (s"$icon: $msg", id) }
Expand Down Expand Up @@ -116,16 +123,16 @@ class FindCommand(using messageOwnership: MessageOwnership, replyCache: ReplyCac
end makeSearchReply

private def getSearchResults(
channel: MessageChannel,
channel: Option[MessageChannel],
guild: Option[Guild],
searchPattern: RE2JPattern
): Seq[(String, String)] =
inline def containsSearchTerm(haystack: String) =
searchPattern.matcher(haystack).nn.find()

var results: Seq[(String, String)] = Vector.empty
channel match
case ch: GuildChannel =>
val guild = ch.getGuild
guild match
case Some(guild) =>
results ++= guild.getRoles.asScala.view
.filter(r => containsSearchTerm(s"@${r.getName}"))
.map(r =>
Expand All @@ -148,11 +155,16 @@ class FindCommand(using messageOwnership: MessageOwnership, replyCache: ReplyCac
.fold("")(name => s" (aka $name)")
(s"**User** ${u.mentionWithName}$nick: `${u.getId}`", u.getId)
)
case _ =>
case None =>
// Private chat
results ++= channel.participants
.filter(u => containsSearchTerm(s"@${u.name}#${u.discriminator}"))
.map(u => (s"**User** ${u.mentionWithName}: `${u.getId}`", u.getId))
channel match
case Some(channel) =>
results ++= channel.participants
.filter(u => containsSearchTerm(s"@${u.name}#${u.discriminator}"))
.map(u => (s"**User** ${u.mentionWithName}: `${u.getId}`", u.getId))
case None =>
logger.warn("Not sure where I am (running find command outside of channel and guild)")
results ++= Vector.fill(10)(("Where am I?", "???")) // Error or creepypasta?
results

object ReactListener extends EventListener:
Expand Down
8 changes: 4 additions & 4 deletions src/main/scala/score/discord/canti/command/QuoteCommand.scala
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class QuoteCommand(messageCache: MessageCache)(using MessageOwnership, ReplyCach
args: String
): Future[Either[String, Message]] =
async {
given JDA = invoker.user.getJDA
parseQuoteIDs(args) match
case Some((quoteId, specifiedChannel)) =>
val channel = channelOrBestGuess(invoker.channel, quoteId, specifiedChannel)
Expand All @@ -92,19 +93,18 @@ class QuoteCommand(messageCache: MessageCache)(using MessageOwnership, ReplyCach
}

private def channelOrBestGuess(
origChannel: MessageChannel,
origChannel: Option[MessageChannel],
quoteId: ID[Message],
specifiedChannel: Option[ID[MessageChannel]]
): Option[MessageChannel] =
given JDA = origChannel.getJDA
)(using JDA): Option[MessageChannel] =
specifiedChannel match
case Some(chanID) => chanID.find
case None =>
messageCache
.find(_.messageId == quoteId)
.map(m => m.chanId)
.flatMap(_.find)
.orElse(Some(origChannel))
.orElse(origChannel)

private def stringifyMessageRetrievalError(
specifiedChannel: Option[ID[MessageChannel]]
Expand Down
11 changes: 7 additions & 4 deletions src/main/scala/score/discord/canti/command/ReadCommand.scala
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,13 @@ class ReadCommand(messageCache: MessageCache)(using Scheduler) extends GenericCo

val rawInput = ctx.args.get(textArg) match
case None =>
val chanId = ctx.invoker.channel.id
messageCache
.find(d => d.chanId == chanId && JAPANESE.findFirstMatchIn(d.text).isDefined)
.map(_.text)
ctx.invoker.channel
.map(_.id)
.flatMap(chanId =>
messageCache
.find(d => d.chanId == chanId && JAPANESE.findFirstMatchIn(d.text).isDefined)
.map(_.text)
)
.getOrElse("")
case Some(text) => text

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ trait CommandInvoker:
def member: Either[String, Member] =
getMember() ?<> "You can only use this command from within a server."

def channel: MessageChannel = getChannel()
def channel: Option[MessageChannel] = getChannel().?

protected def getUser(): User

protected def getMember(): Member | Null

protected def getChannel(): MessageChannel
protected def getChannel(): MessageChannel | Null

def replyLater(transientIfPossible: Boolean)(using Scheduler): Future[Unit]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ object Commands:

def logCommandInvocation(invoker: CommandInvoker, cmd: GenericCommand): Unit =
logger.debug(
s"Running command '${cmd.name}' on behalf of ${invoker.user.unambiguousString} in ${invoker.channel.unambiguousString}"
s"Running command '${cmd.name}' on behalf of ${invoker.user.unambiguousString} in ${invoker.channel
.fold("<unknown>")(_.unambiguousString)}"
)
end Commands

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class SlashCommands(commands: GenericCommand*)(using MessageOwnership, ReplyCach
case Some(cmd) =>
val guildStr = ev.getGuild.?.fold("no guild")(_.unambiguousString)
logger.debug(
s"Running slash command ${cmd.name} on behalf of user ${ev.getUser.unambiguousString} in ${ev.getChannel.unambiguousString} ($guildStr)"
s"Running slash command ${cmd.name} on behalf of user ${ev.getUser.unambiguousString} in ${ev.getChannel.?.fold("<unknown channel>")(_.unambiguousString)} ($guildStr)"
)
val invoker = SlashCommandInvoker(ev)
Future {
Expand Down
31 changes: 19 additions & 12 deletions src/main/scala/score/discord/canti/functionality/Spoilers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import score.discord.canti.command.GenericCommand
import score.discord.canti.functionality.ownership.MessageOwnership
import score.discord.canti.util.{APIHelper, BotMessages}
import score.discord.canti.wrappers.NullWrappers.*
import score.discord.canti.wrappers.jda.{ID, RetrievableMessage}
import score.discord.canti.wrappers.jda.{ID, MessageReceiver, RetrievableMessage}
import score.discord.canti.wrappers.jda.MessageConversions.*
import score.discord.canti.wrappers.jda.RichMessage.!
import score.discord.canti.wrappers.jda.RichMessageChannel.{mention, sendOwned}
import score.discord.canti.wrappers.jda.RichRestAction.queueFuture
Expand Down Expand Up @@ -82,34 +83,40 @@ class Spoilers(spoilerTexts: AsyncMap[ID[Message], String], conversations: Conve
case None =>
await(createSpoilerConversation(ctx.invoker))
case Some(trimmed) =>
await(createSpoiler(ctx.invoker.channel, ctx.invoker.user, trimmed))
await(createSpoiler(ctx.invoker.asMessageReceiver, ctx.invoker.user, trimmed))
}

private def createSpoilerConversation(invoker: CommandInvoker): Future[RetrievableMessage] =
val channel = invoker.channel
for
privateChannel <- invoker.user.openPrivateChannel().queueFuture()
message <- privateChannel
.sendMessage(
s"Please enter your spoiler contents for ${channel.mention}, or reply with 'cancel' to cancel."
)
.sendMessage({
val channelText = channel.fold("")(c => s" for ${c.mention}")
s"Please enter your spoiler contents$channelText, or reply with 'cancel' to cancel."
})
.queueFuture()
yield
conversations.start(message.getAuthor, privateChannel) { conversation =>
conversation.message.getContentRaw match
case "cancel" =>
conversation.message.!("Did not create a spoiler.")
case spoiler =>
for _ <- createSpoiler(channel, conversation.message.getAuthor, spoiler) do
conversation.message.!("Created your spoiler.")
for
_ <- createSpoiler(
invoker.asMessageReceiver,
conversation.message.getAuthor,
spoiler
)
do conversation.message.!("Created your spoiler.")
}
RetrievableMessage(message)
end spoilerCommand

val allCommands: Seq[GenericCommand] = Seq(spoilerCommand)

private def createSpoiler(
spoilerChannel: MessageChannel,
replyHook: MessageReceiver,
author: User,
args: String
): Future[RetrievableMessage] =
Expand All @@ -124,15 +131,15 @@ class Spoilers(spoilerTexts: AsyncMap[ID[Message], String], conversations: Conve

val hintText = if hintTextMaybe.isEmpty then "spoilers" else hintTextMaybe

val spoilerMessage = await(
spoilerChannel.sendOwned(
val spoilerMessageHook = await(
replyHook.sendMessage(
BotMessages.okay(
s"**Click the magnifying glass** to see ${hintText.trim} (from ${author.mentionWithName})"
),
owner = author
): MessageFromX
)
)

val spoilerMessage = await(spoilerMessageHook.retrieve())
val spoilerDbUpdate =
spoilerTexts(spoilerMessage.id) = spoilerText.trimnn
await(spoilerMessage.addReaction(spoilerEmote).queueFuture())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ class VoiceKick(
override def canBeEdited = false

override def execute(ctx: CommandInvocation): Future[RetrievableMessage] = async {
val textChannel = ctx.invoker.channel
val result = for
member <- ctx.invoker.member
guild = member.getGuild
Expand All @@ -130,7 +129,7 @@ class VoiceKick(
"You cannot kick a user from the guild AFK channel"
)

guildTextChannel <- ensureIsGuildTextChannel(textChannel)
guildTextChannel <- ensureIsGuildTextChannel(ctx.invoker.channel)

mentionedUser <- singleMentionedUser(ctx.args(kickUserArg))
mentioned <- guild.getMember(mentionedUser) ?<> "Cannot find that user in this server"
Expand Down Expand Up @@ -183,8 +182,8 @@ class VoiceKick(
case Right((kickState, guildTextChannel, successMsg, voiceChan, mentioned)) =>
await(ownerByChannel(voiceChan)) match
case Some(owner) if owner == ctx.invoker.user =>
addTemporaryVoiceBan(voiceChan, mentioned, MessageReceiver(textChannel))
kickVoiceMember(voiceChan, mentioned, textChannel)
addTemporaryVoiceBan(voiceChan, mentioned, MessageReceiver(guildTextChannel))
kickVoiceMember(voiceChan, mentioned, guildTextChannel)
ctx.invoker.reply(
BotMessages.okay(
s"${mentioned.getAsMention} was forcibly kicked from #${voiceChan.name} by the owner ${owner.getAsMention}"
Expand Down Expand Up @@ -219,9 +218,12 @@ class VoiceKick(
await(msg)
}

private def ensureIsGuildTextChannel(textChannel: MessageChannel): Either[String, TextChannel] =
private def ensureIsGuildTextChannel(
textChannel: Option[MessageChannel]
): Either[String, TextChannel] =
textChannel match
case c: TextChannel => Right(c)
case Some(c: TextChannel) => Right(c)
case None => Left("I cannot read the member list of this channel") // Executed in thread
case _ =>
Left(
"Internal error: Command not run from within a guild, but `message.getMember()` disagrees"
Expand Down Expand Up @@ -336,10 +338,9 @@ class VoiceKick(
// XXX Oh my god static mutable globals in a multithreaded environment
// XXX Hack: JDA seems to consistently get the wrong idea about permissions here for some reason.
Manager.setPermissionChecksEnabled(false)
try
voiceChannel.applyPerms(
PermissionCollection(member.asPermissionHolder -> permsWithVoiceBan)
)
try voiceChannel.applyPerms(
PermissionCollection(member.asPermissionHolder -> permsWithVoiceBan)
)
finally Manager.setPermissionChecksEnabled(true)
},
onFail = APIHelper.loudFailure(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package score.discord.canti.wrappers.jda

import net.dv8tion.jda.api.entities.{GuildChannel}
import net.dv8tion.jda.api.entities.GuildChannel
import net.dv8tion.jda.api.requests.restaction.ChannelAction
import score.discord.canti.discord.permissions.{PermissionCollection, PermissionHolder}
import score.discord.canti.wrappers.NullWrappers.*
Expand Down

0 comments on commit d799598

Please sign in to comment.