Skip to content

Commit

Permalink
Fixes to get command interactions to work
Browse files Browse the repository at this point in the history
  • Loading branch information
Katrix committed Oct 16, 2023
1 parent 0162094 commit e695c6b
Show file tree
Hide file tree
Showing 13 changed files with 197 additions and 119 deletions.
2 changes: 1 addition & 1 deletion ackcord/src/main/scala/ackcord/BotSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ object BotSettings {
handleReconnect: HandleReconnect[F],
supervisor: Supervisor[F],
processors: Seq[GatewayProcessComponent[F]]
)(implicit F: _root_.cats.MonadError[F, Throwable], doAsync: DoAsync[F])
)(implicit F: _root_.cats.MonadError[F, Throwable], val doAsync: DoAsync[F])
extends BotSettings[F, P, Handler] {
type Self = NormalBotSettings[F, P, Handler]

Expand Down
18 changes: 15 additions & 3 deletions data/src/main/scala/ackcord/data/UndefOr.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package ackcord.data

import io.circe.{Decoder, HCursor}
import cats.data.{NonEmptyList, Validated}
import io.circe.Decoder.{AccumulatingResult, Result}
import io.circe.{ACursor, Decoder, HCursor}

sealed trait UndefOr[+A] {
def isUndefined: Boolean
Expand Down Expand Up @@ -31,8 +33,18 @@ sealed trait UndefOr[+A] {
def get: A = toEither.toTry.get
}
object UndefOr {
implicit def undefOrDecoder[A: Decoder]: Decoder[UndefOr[A]] = (c: HCursor) =>
if (c.succeeded) c.as[A].map(UndefOrSome(_)) else Right(UndefOrUndefined())
implicit def undefOrDecoder[A: Decoder]: Decoder[UndefOr[A]] = new Decoder[UndefOr[A]] {

override def apply(c: HCursor): Result[UndefOr[A]] =
if (c.succeeded) c.as[A].map(UndefOrSome(_)) else Right(UndefOrUndefined())

override def tryDecode(c: ACursor): Result[UndefOr[A]] =
if (c.succeeded) c.as[A].map(UndefOrSome(_)) else Right(UndefOrUndefined())

override def tryDecodeAccumulating(c: ACursor): AccumulatingResult[UndefOr[A]] =
if (c.succeeded) Validated.fromEither(c.as[A].map(UndefOrSome(_))).leftMap(NonEmptyList.one)
else Validated.Valid(UndefOrUndefined())
}

def fromOption[A](opt: Option[A]): UndefOr[A] = opt match {
case Some(value) => UndefOrSome(value)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
package ackcord.data.base

import io.circe.Json

class MissingFieldException private (val message: String, val missingOn: AnyRef) extends Exception(message)
object MissingFieldException {
def default(field: String, obj: AnyRef): MissingFieldException =
new MissingFieldException(s"Missing field $field on object $obj", obj)
obj match {
case json: Json =>
if (json.hcursor.downField(field).succeeded)
new MissingFieldException(s"Found field $field on object $obj, but count not decode it", obj)
else new MissingFieldException(s"Missing field $field on object $obj", obj)

case _ => new MissingFieldException(s"Missing field $field on object $obj", obj)
}

def messageAndData(message: String, missingOn: AnyRef): MissingFieldException =
new MissingFieldException(message, missingOn)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>[%-5level] [%date{ISO8601}] [%logger{36}] [%X{sourceThread}] %msg%n</pattern>
<pattern>[%-5level] [%date{ISO8601}] [%logger{36}] %msg%n</pattern>
</encoder>
</appender>

Expand Down
101 changes: 75 additions & 26 deletions example/src/main/scala/ackcordexample/Testing.scala
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package ackcordexample

import ackcord.BotSettings
import ackcord.data.Component.ActionRow
import ackcord.data._
import ackcord.gateway._
import ackcord.gateway.data.{GatewayDispatchEvent, GatewayEventBase}
import ackcord.interactions.{
ApplicationCommandController,
Components,
CreatedApplicationCommand,
HighInteractionResponse,
InteractionHandlerOps,
InteractionsRegistrar,
SlashCommand
}
Expand Down Expand Up @@ -51,8 +54,8 @@ object Testing extends ResourceApp {
console.println(behavior)
}

class MyCommands(requests: Requests[IO, Any], components: Components[IO], respondToPing: Boolean)
extends ApplicationCommandController.Base[IO](requests, components, respondToPing) {
class MyCommands(requests: Requests[IO, Any], components: Components[IO])
extends ApplicationCommandController.Base[IO](requests, components) {
val infoCommand: SlashCommand[IO, Unit] = SlashCommand.command("info", "Get some info") { invocation =>
sendMessage(
MessageData.ofContent("Hello from AckCord 2.0 application commands")
Expand All @@ -63,7 +66,7 @@ object Testing extends ResourceApp {
}
}

val infoCommandAsync: SlashCommand[IO, Unit] = SlashCommand.command("infoAsync", "Get some info async") {
val infoCommandAsync: SlashCommand[IO, Unit] = SlashCommand.command("infoasync", "Get some info async") {
invocation =>
async(invocation) { implicit a =>
sendAsyncMessage(
Expand All @@ -76,6 +79,9 @@ object Testing extends ResourceApp {
infoCommand,
infoCommandAsync
)

override def onDispatchEvent(event: GatewayDispatchEvent, context: Context): IO[Unit] =
super.onDispatchEvent(event, context)
}

override def run(args: List[String]): Resource[IO, ExitCode] = {
Expand All @@ -84,32 +90,42 @@ object Testing extends ResourceApp {
BotSettings
.cats(
token,
GatewayIntents.Guilds ++ GatewayIntents.GuildMessages ++ GatewayIntents.MessageContent,
GatewayIntents.values.foldLeft(GatewayIntents.unknown(0))(
_ ++ _
), //GatewayIntents.Guilds ++ GatewayIntents.GuildMessages ++ GatewayIntents.MessageContent ++ GatewayIntents.GuildIntegrations,
HttpClientCatsBackend.resource[IO]()
)
.map { s =>
import s.doAsync
s.copy(
logGatewayMessages = true
)
}
.mproduct { settings =>
Resource.eval(
Components
.ofCats[IO](settings.requests, respondToPing = false)
.map(new MyCommands(settings.requests, _, respondToPing = true))
Components.ofCats[IO](settings.requests)
)
}
.map { case (settings, commands) =>
settings
.installEventListener { case ready: GatewayDispatchEvent.Ready =>
InteractionsRegistrar
.createGuildCommands[IO](
ready.application.id,
GuildId("269988507378909186": String),
settings.requests,
replaceAll = true,
commands.allCommands: _*
)
.void
}
.install(commands)
.map { case (settings, components) =>
val commands = new MyCommands(settings.requests, components)
(
settings
.installEventListener { case ready: GatewayDispatchEvent.Ready =>
InteractionsRegistrar
.createGuildCommands[IO](
ready.application.id,
GuildId("269988507378909186": String),
settings.requests,
replaceAll = true,
commands.allCommands: _*
)
.void
}
.install(commands),
components
)
}
.map { settings =>
.map { case (settings, components) =>
val console = Console[IO]
val req = settings.requests

Expand All @@ -120,15 +136,15 @@ object Testing extends ResourceApp {
console.println("Ready")
}
.install(
new PrintlnProcessor(console, settings.handlerFactory.handlerContextKey),
//new PrintlnProcessor(console, settings.handlerFactory.handlerContextKey),
new StringCommandProcessor(
Map(
"!info" -> ((_, channelId) => {
req
.runRequest(
ChannelRequests.createMessage(
channelId,
ChannelRequests.CreateMessageBody.make20(content = UndefOrSome("Hello from AckCord 2.0"))
ChannelRequests.CreateMessageBody.ofContent("Hello from AckCord 2.0")
)
)
.void
Expand All @@ -138,11 +154,44 @@ object Testing extends ResourceApp {
.tabulate(content.substring("!ratelimitTest ".length).toInt)(i =>
ChannelRequests.createMessage(
channelId,
ChannelRequests.CreateMessageBody
.make20(content = UndefOrSome(s"Ratelimit message ${i + 1}"))
ChannelRequests.CreateMessageBody.ofContent(s"Ratelimit message ${i + 1}")
)
)
.traverse_(req.runRequest)
}),
"!componentTest" -> ((_, channelId) => {

for {
btn <- components.buttons.make(label = Some("Press me")) { _ =>
new InteractionHandlerOps[IO] {
override def requests: Requests[IO, Any] = req

override protected def doNothing: IO[Unit] = IO.unit

val res: IO[HighInteractionResponse.AsyncMessageable[IO]] =
IO.pure(sendMessage(MessageData.ofContent("You pressed the button")))
}.res
}
_ <- req.runRequest(
ChannelRequests.createMessage(
channelId,
ChannelRequests.CreateMessageBody.ofContent(
"Here's a button to press",
components = Seq(
ActionRow.make20(
components = Seq(btn)
)
)
)
)
)
} yield ()
}),
"!raiseError" -> ((_, _) => {
IO.raiseError(new Exception("Some error here"))
}),
"!throwError" -> ((_, _) => {
throw new Exception("Some error here")
})
),
log
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ trait GatewayProcessComponent[F[_]] { self =>

def onEventAll(event: GatewayEventBase[_], context: Context)(implicit alter: FAlter[F]): F[Context] = {
implicit val m: Monad[F] = F

alter
.alter(onEvent(event, context), makeReturn(context))
.map(r => valueFromReturn(r).getOrElse(context))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,65 +104,69 @@ object CatsGatewayHandlerFactory {
ws.send(WebSocketFrame.Close(code, reason)) *> reconnect(true)

private def receiveSingleEvent(supervisor: Supervisor[F], inflater: Inflate.PureInflater[F]): F[Unit] =
receiveRawGatewayEvent(inflater).flatMap {
case Some(js) =>
GatewayEvent.tryDecode(js) match {
case Right(ev) =>
val handleExternally = handle
.onEvent(ev, Context.empty.add(handlerContextKey, this))
.void
.handleError { e =>
log.error(e)("Encountered error while handling event")
}
.race(
Temporal[F].sleep(3.seconds) *> log.error(new Exception("Event execution took too long"))(
"Handling of event too too long and has timed out. Make sure to not block in event handlers"
receiveRawGatewayEvent(inflater)
.flatMap {
case Some(js) =>
GatewayEvent.tryDecode(js) match {
case Right(ev) =>
val handleExternally = handle
.onEvent(ev, Context.empty.add(handlerContextKey, this))
.void
.handleErrorWith { e =>
log.error(e)("Encountered error while handling event")
}
.race(
Temporal[F].sleep(3.seconds) *> log.error(new Exception("Event execution took too long"))(
"Handling of event too too long and has timed out. Make sure to not block in event handlers"
)
)
)
.void

val actOnEvent = ev match {
case GatewayEvent.Dispatch(ev) =>
val setSeq = lastReceivedSeqRef.set(Some(ev.s))

//We don't access to dispatch type for a tiny bit more safety. Don't want to crash here
val setResumeDataEither = if (ev.t == GatewayDispatchType.Ready) {
val d = ev.d.json.hcursor
.void

val actOnEvent = ev match {
case GatewayEvent.Dispatch(ev) =>
val setSeq = lastReceivedSeqRef.set(Some(ev.s))

//We don't access to dispatch type for a tiny bit more safety. Don't want to crash here
val setResumeDataEither = if (ev.t == GatewayDispatchType.Ready) {
val d = ev.d.json.hcursor
for {
resumeGatewayUrl <- d.get[String]("resume_gateway_url")
sessionId <- d.get[String]("session_id")
} yield for {
_ <- resumeGatewayUrlRef.set(Some(resumeGatewayUrl))
_ <- sessionIdRef.set(Some(sessionId))
} yield ()
} else Right(().pure)

setResumeDataEither match {
case Right(setRefs) => setSeq *> setRefs
case Left(_) =>
val reason =
s"Received ${GatewayDispatchType.Ready.value} with invalid resume_gateway_url or session_id"
log.warn(reason) *> disconnectAndReconnect(reason, code = 4002)
}
case GatewayEvent.Reconnect(_) => reconnect(true)
case GatewayEvent.InvalidSession(ev) => reconnect(ev.d)
case GatewayEvent.Hello(ev) =>
for {
resumeGatewayUrl <- d.get[String]("resume_gateway_url")
sessionId <- d.get[String]("session_id")
} yield for {
_ <- resumeGatewayUrlRef.set(Some(resumeGatewayUrl))
_ <- sessionIdRef.set(Some(sessionId))
_ <- startHeartbeat(ev.d.heartbeatInterval.millis, supervisor)
_ <- sendIdentify
} yield ()
} else Right(().pure)

setResumeDataEither match {
case Right(setRefs) => setSeq *> setRefs
case Left(_) =>
val reason =
s"Received ${GatewayDispatchType.Ready.value} with invalid resume_gateway_url or session_id"
log.warn(reason) *> disconnectAndReconnect(reason, code = 4002)
}
case GatewayEvent.Reconnect(_) => reconnect(true)
case GatewayEvent.InvalidSession(ev) => reconnect(ev.d)
case GatewayEvent.Hello(ev) =>
for {
_ <- startHeartbeat(ev.d.heartbeatInterval.millis, supervisor)
_ <- sendIdentify
} yield ()

case GatewayEvent.Heartbeat(_) => heartbeatNowQueue.offer(())
case GatewayEvent.HeartbeatACK(_) => receivedHeartbeatAckRef.set(true)
case _ => Applicative[F].unit
}

Concurrent[F].uncancelable(p => p(actOnEvent) *> handleExternally)
case Left(_) =>
disconnectAndReconnect("Received payload with invalid op field", code = 4002).widen
}
case None => ().pure
}

case GatewayEvent.Heartbeat(_) => heartbeatNowQueue.offer(())
case GatewayEvent.HeartbeatACK(_) => receivedHeartbeatAckRef.set(true)
case _ => Applicative[F].unit
}

Concurrent[F].uncancelable(p => p(actOnEvent) *> handleExternally)
case Left(_) =>
disconnectAndReconnect("Received payload with invalid op field", code = 4002).widen
}
case None => ().pure
}
.handleErrorWith { e =>
log.error(e)("Exception raised while handling WebSocket event. Please report this to AckCord")
}

private def checkHeartbeatAckReceived: F[Unit] = receivedHeartbeatAckRef.get.ifM(
ifTrue = ().pure,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ fields:
innerTypes:
- name: ApplicationCommandType
defType: Enum
type: String
type: Int
values:
ChatInput:
value: "1"
Expand Down
Loading

0 comments on commit e695c6b

Please sign in to comment.