Skip to content

Commit

Permalink
Add buttons to &find too
Browse files Browse the repository at this point in the history
  • Loading branch information
ScoreUnder committed Jun 13, 2021
1 parent 7f0f254 commit d264ec0
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 49 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package score.discord.canti.command

import net.dv8tion.jda.api.entities.Message
import net.dv8tion.jda.api.requests.restaction.MessageAction
import score.discord.canti.collections.ReplyCache
import score.discord.canti.functionality.ownership.MessageOwnership
import score.discord.canti.util.APIHelper
import score.discord.canti.wrappers.jda.Conversions._
import score.discord.canti.wrappers.jda.ID

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.{ExecutionContext, Future}
import scala.util.chaining.scalaUtilChainingOps

trait DataReplyingCommand[T] extends ReplyingCommand {
override final def executeAndGetMessage(message: Message, args: String): Future[Message] =
executeAndGetMessageWithData(message, args).map(_._1)(ExecutionContext.parasitic)

def executeAndGetMessageWithData(message: Message, args: String): Future[(Message, T)]

override def executeFuture(message: Message, args: String): Future[Message] =
for {
(replyUnsent, data) <- executeAndGetMessageWithData(message, args)
reply <-
message.reply(replyUnsent)
.mentionRepliedUser(false)
.pipe(tweakMessageAction(_, data))
.queueFuture()
.tap(message.registerReply)
} yield reply

def tweakMessageAction(action: MessageAction, data: T): MessageAction

override def executeForEdit(message: Message, myMessageOption: Option[ID[Message]], args: String): Unit =
for (oldMessage <- myMessageOption; (myReply, data) <- executeAndGetMessageWithData(message, args)) {
APIHelper.tryRequest(message.getChannel.editMessageById(oldMessage.value, myReply).pipe(tweakMessageAction(_, data)),
onFail = APIHelper.failure("executing a command for edited message"))
}
}
93 changes: 69 additions & 24 deletions src/main/scala/score/discord/canti/command/FindCommand.scala
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
package score.discord.canti.command

import com.google.re2j.{PatternSyntaxException, Pattern => RE2JPattern}
import net.dv8tion.jda.api.entities.Message
import net.dv8tion.jda.api.entities.{Message, MessageChannel}
import net.dv8tion.jda.api.events.GenericEvent
import net.dv8tion.jda.api.events.interaction.ButtonClickEvent
import net.dv8tion.jda.api.hooks.EventListener
import net.dv8tion.jda.api.interactions.components.{ActionRow, Button}
import net.dv8tion.jda.api.requests.restaction.MessageAction
import net.dv8tion.jda.api.{EmbedBuilder, JDA}
import score.discord.canti.collections.ReplyCache
import score.discord.canti.functionality.ownership.MessageOwnership
import score.discord.canti.util.{APIHelper, BotMessages, MessageUtils}
import score.discord.canti.wrappers.jda.Conversions._
import score.discord.canti.wrappers.jda.ID
import score.discord.canti.wrappers.jda.matching.Events.NonBotReact
import score.discord.canti.wrappers.jda.matching.React

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.jdk.CollectionConverters._
import scala.util.Try
import scala.util.chaining.scalaUtilChainingOps

class FindCommand(implicit val messageOwnership: MessageOwnership, val replyCache: ReplyCache) extends Command.Anyone with ReplyingCommand {
class FindCommand(implicit val messageOwnership: MessageOwnership, val replyCache: ReplyCache) extends Command.Anyone with DataReplyingCommand[Seq[String]] {
override def name: String = "find"

override val aliases: Seq[String] = List("id")
Expand All @@ -36,27 +41,27 @@ class FindCommand(implicit val messageOwnership: MessageOwnership, val replyCach
|So far, this searches roles, emotes, users, and games.
""".stripMargin

override def executeAndGetMessage(message: Message, args: String): Future[Message] =
override def executeAndGetMessageWithData(message: Message, args: String): Future[(Message, Seq[String])] =
Future {
(args.trim match {
case "" => BotMessages.error("Please enter a term to search for.")
case searchTerm => makeSearchReply(message, searchTerm)
}).toMessage
args.trim match {
case "" => (BotMessages.error("Please enter a term to search for.").toMessage, Nil)
case searchTerm => makeSearchReply(message, searchTerm).pipe { case (x, y) => (x.toMessage, y) }
}
}

private def makeSearchReply(message: Message, searchTerm: String): EmbedBuilder = {
private def makeSearchReply(message: Message, searchTerm: String): (EmbedBuilder, Seq[String]) = {
val maxResults = 10
val searchTermSanitised = MessageUtils.sanitiseCode(searchTerm)
Try(RE2JPattern.compile(searchTerm, RE2JPattern.CASE_INSENSITIVE))
.map { searchPattern =>
val results = getSearchResults(message, searchPattern)
.take(maxResults + 1)
.zip(ReactListener.ICONS.iterator ++ Iterator.continually(""))
.map { case (msg, icon) => s"$icon: $msg" }
.map { case ((msg, id), icon) => (s"$icon: $msg", id) }
.toVector

if (results.isEmpty) {
BotMessages.plain(s"No results found for ``$searchTermSanitised``")
(BotMessages.plain(s"No results found for ``$searchTermSanitised``"), Nil)
} else {
val header =
if (results.size > maxResults)
Expand All @@ -72,34 +77,47 @@ class FindCommand(implicit val messageOwnership: MessageOwnership, val replyCach
else
"\n") + ReactListener.SEARCHABLE_MESSAGE_TAG

BotMessages.okay(s"$header\n${results take maxResults mkString "\n"}$footer")
(BotMessages.okay(s"$header\n${results take maxResults map (_._1) mkString "\n"}$footer"),
results.take(maxResults).map(_._2))
}
}
.recover {
case e: PatternSyntaxException =>
BotMessages.error(s"Could not parse regex for $name command: ${e.getDescription}")
(BotMessages.error(s"Could not parse regex for $name command: ${e.getDescription}"), Nil)
}
.get
}

private def getSearchResults(message: Message, searchPattern: RE2JPattern): Seq[String] = {

override def tweakMessageAction(action: MessageAction, data: Seq[String]): MessageAction =
action.setActionRows(
data
.zipWithIndex
.map { case (id, index) => Button.secondary(ReactListener.ACTION_PREFIX + id, index.toString) }
.grouped(5)
.map(buttons => ActionRow.of(buttons.asJava))
.toSeq
.asJava
)

private def getSearchResults(message: Message, searchPattern: RE2JPattern): Seq[(String, String)] = {
@inline def containsSearchTerm(haystack: String) =
searchPattern.matcher(haystack).find()

var results: Seq[String] = Vector.empty
var results: Seq[(String, String)] = Vector.empty
message.guild match {
case None =>
// Private chat
results ++= message.getChannel.participants
.filter(u => containsSearchTerm(s"@${u.name}#${u.discriminator}"))
.map(u => s"**User** ${u.mentionWithName}: `${u.getId}`")
.map(u => (s"**User** ${u.mentionWithName}: `${u.getId}`", u.getId))
case Some(guild) =>
results ++= guild.getRoles.asScala.view
.filter(r => containsSearchTerm(s"@${r.getName}"))
.map(r => s"**Role** ${r.getAsMention} (${MessageUtils.sanitise(s"@${r.getName}")}): `${r.getId}`")
.map(r => (s"**Role** ${r.getAsMention} (${MessageUtils.sanitise(s"@${r.getName}")}): `${r.getId}`", r.getId))
results ++= guild.getEmotes.asScala.view
.filter(e => containsSearchTerm(s":${e.getName}:"))
.map(e => s"**Emote** ${e.getAsMention} (:${e.getName}:): `${e.getId}`")
.map(e => (s"**Emote** ${e.getAsMention} (:${e.getName}:): `${e.getId}`", e.getId))
results ++= guild.getMembers.asScala.view
.filter(m =>
containsSearchTerm(s"@${m.getUser.name}#${m.getUser.discriminator}") ||
Expand All @@ -108,33 +126,60 @@ class FindCommand(implicit val messageOwnership: MessageOwnership, val replyCach
val u = m.getUser
val nick = Option(m.getNickname)
.map(MessageUtils.sanitise)
.map(name => s" (aka $name)")
.getOrElse("")
s"**User** ${u.mentionWithName}$nick: `${u.getId}`"
.fold("")(name => s" (aka $name)")
(s"**User** ${u.mentionWithName}$nick: `${u.getId}`", u.getId)
})
}
results
}

object ReactListener extends EventListener {
val ICONS = Vector("0️⃣", "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "\uD83D\uDD1F")
val ACTION_PREFIX = "show_result:"
val SEARCHABLE_MESSAGE_TAG = "React with one of the icons above to make it easier to copy the ID on mobile"
private val LINE_REGEX = (ICONS.mkString("(", "|", ")") + ":.*`(\\d+)`").r.unanchored

def getIdFromMessage(channel: MessageChannel, myMsgId: ID[Message], idLabel: String): Future[Option[(Message, String)]] = {
for {
msg <- APIHelper.tryRequest(channel.retrieveMessageById(myMsgId.value), onFail = APIHelper.failure("retrieving reacted message"))
} yield {
(for {
embed <- msg.getEmbeds.asScala
if embed.getDescription.contains(SEARCHABLE_MESSAGE_TAG)
LINE_REGEX(`idLabel`, selected) <- embed.getDescription.split("\n")
} yield (msg, selected)).headOption
}
}

override def onEvent(event: GenericEvent): Unit = event match {
case NonBotReact(React.Text(react), msgId, channel, user) =>
implicit val jda: JDA = event.getJDA
if (ICONS contains react) {
for {
Some(`user`) <- messageOwnership(msgId)
msg <- APIHelper.tryRequest(channel.retrieveMessageById(msgId.value), onFail = APIHelper.failure("retrieving reacted message"))
embed <- msg.getEmbeds.asScala
if embed.getDescription.contains(SEARCHABLE_MESSAGE_TAG)
LINE_REGEX(`react`, selected) <- embed.getDescription.split("\n")
maybeMsgId <- getIdFromMessage(channel, msgId, react)
msgId <- maybeMsgId
(msg, selected) = msgId
} {
APIHelper.tryRequest(msg.editMessage(selected), onFail = APIHelper.failure("editing message for reaction"))
}
}
case ev: ButtonClickEvent =>
implicit val jda = event.getJDA
val rawId = ev.getComponentId
if (rawId.startsWith(ACTION_PREFIX)) {
val id = rawId.substring(ACTION_PREFIX.length)
for {
owner <- messageOwnership(new ID[Message](ev.getMessageIdLong))
if id.forall(c => c.isDigit || c == '-') // Sanity check for potential exploits that probably don't exist
} {
val user = ev.getUser
owner match {
case Some(`user`) => ev.editMessage(id).queueFuture()
case _ => ev.reply(id).setEphemeral(true).queueFuture()
}
}
}
case _ =>
}
}
Expand Down
29 changes: 4 additions & 25 deletions src/main/scala/score/discord/canti/command/QuoteCommand.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ import scala.async.Async._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.jdk.CollectionConverters._
import scala.util.chaining._

class QuoteCommand(implicit messageCache: MessageCache, val messageOwnership: MessageOwnership, val replyCache: ReplyCache) extends Command.Anyone with ReplyingCommand {
class QuoteCommand(implicit messageCache: MessageCache, val messageOwnership: MessageOwnership, val replyCache: ReplyCache) extends Command.Anyone with DataReplyingCommand[Option[String]] {
override def name: String = "quote"

override val aliases: Seq[String] = List("q")
Expand All @@ -40,10 +39,7 @@ class QuoteCommand(implicit messageCache: MessageCache, val messageOwnership: Me
|`>>12341234`
""".stripMargin

def executeAndGetMessage(cmdMessage: Message, args: String): Future[Message] =
executeAndGetMessageWithUrl(cmdMessage, args).map(_._1)

def executeAndGetMessageWithUrl(cmdMessage: Message, args: String): Future[(Message, Option[String])] =
override def executeAndGetMessageWithData(cmdMessage: Message, args: String): Future[(Message, Option[String])] =
async {
val quotedMsg = await(retrieveQuoteMessageByArg(cmdMessage, args))
val replyMsg = quotedMsg
Expand All @@ -70,29 +66,12 @@ class QuoteCommand(implicit messageCache: MessageCache, val messageOwnership: Me
}
}

def addLink(action: MessageAction, link: Option[String]): MessageAction =
link match {
override def tweakMessageAction(action: MessageAction, data: Option[String]): MessageAction =
data match {
case None => action
case Some(link) => action.setActionRow(Button.link(link, "Go to message"))
}

override def executeFuture(message: Message, args: String): Future[Message] =
for {
(replyUnsent, url) <- executeAndGetMessageWithUrl(message, args)
reply <-
message.reply(replyUnsent)
.mentionRepliedUser(false)
.pipe(addLink(_, url))
.queueFuture()
.tap(message.registerReply)
} yield reply

override def executeForEdit(message: Message, myMessageOption: Option[ID[Message]], args: String): Unit =
for (oldMessage <- myMessageOption; (myReply, url) <- executeAndGetMessageWithUrl(message, args)) {
APIHelper.tryRequest(message.getChannel.editMessageById(oldMessage.value, myReply).pipe(addLink(_, url)),
onFail = APIHelper.failure("executing a command for edited message"))
}

private def channelOrBestGuess(context: Message, quoteId: ID[Message], specifiedChannel: Option[ID[MessageChannel]]): Option[MessageChannel] = {
implicit val jda: JDA = context.getJDA
specifiedChannel match {
Expand Down

0 comments on commit d264ec0

Please sign in to comment.