Skip to content

Commit

Permalink
use sync predicates for interactions
Browse files Browse the repository at this point in the history
  • Loading branch information
tim-smart committed Apr 10, 2024
1 parent 372f842 commit f98ef9e
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 157 deletions.
7 changes: 2 additions & 5 deletions src/Interactions/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,9 @@ export const ModalSubmitData = GenericTag<
export interface DiscordFocusedOption {
readonly _: unique symbol
}
export interface FocusedOptionContext {
readonly focusedOption: Discord.ApplicationCommandInteractionDataOption
}
export const FocusedOptionContext = GenericTag<
DiscordFocusedOption,
FocusedOptionContext
Discord.ApplicationCommandInteractionDataOption
>("dfx/Interactions/FocusedOptionContext")

export interface DiscordSubCommand {
Expand Down Expand Up @@ -78,7 +75,7 @@ export const resolved = <A>(

export const focusedOptionValue = Effect.map(
FocusedOptionContext,
_ => _.focusedOption.value ?? "",
_ => _.value ?? "",
)

export class SubCommandNotFound extends TypeIdError(
Expand Down
60 changes: 23 additions & 37 deletions src/Interactions/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ export class GlobalApplicationCommand<R, E> {
export const global = <
R,
E,
const A extends
DeepReadonlyObject<Discord.CreateGlobalApplicationCommandParams>,
const A extends Discord.CreateGlobalApplicationCommandParams,
>(
command: A,
handle: CommandHandler<R, E, A>,
Expand All @@ -52,8 +51,7 @@ export class GuildApplicationCommand<R, E> {
export const guild = <
R,
E,
const A extends
DeepReadonlyObject<Discord.CreateGuildApplicationCommandParams>,
const A extends Discord.CreateGuildApplicationCommandParams,
>(
command: A,
handle: CommandHandler<R, E, A>,
Expand All @@ -66,79 +64,67 @@ export const guild = <
export class MessageComponent<R, E> {
readonly _tag = "MessageComponent"
constructor(
readonly predicate: (customId: string) => Effect.Effect<boolean, E, R>,
readonly predicate: (customId: string) => boolean,
readonly handle: Effect.Effect<Discord.InteractionResponse, E, R>,
) {}
}

export const messageComponent = <R1, R2, E1, E2>(
pred: (customId: string) => Effect.Effect<boolean, E1, R1>,
handle: CommandHandler<R2, E2, Discord.InteractionResponse>,
export const messageComponent = <R, E>(
pred: (customId: string) => boolean,
handle: CommandHandler<R, E, Discord.InteractionResponse>,
) =>
new MessageComponent<
Exclude<R1 | R2, DiscordInteraction | DiscordMessageComponent | Scope>,
E1 | E2
>(pred as any, handle as any)
Exclude<R, DiscordInteraction | DiscordMessageComponent | Scope>,
E
>(pred, handle as any)

export class ModalSubmit<R, E> {
readonly _tag = "ModalSubmit"
constructor(
readonly predicate: (customId: string) => Effect.Effect<boolean, E, R>,
readonly predicate: (customId: string) => boolean,
readonly handle: Effect.Effect<Discord.InteractionResponse, E, R>,
) {}
}

export const modalSubmit = <R1, R2, E1, E2>(
pred: (customId: string) => Effect.Effect<boolean, E1, R1>,
handle: Effect.Effect<Discord.InteractionResponse, E2, R2>,
export const modalSubmit = <R, E>(
pred: (customId: string) => boolean,
handle: Effect.Effect<Discord.InteractionResponse, E, R>,
) =>
new ModalSubmit<
Exclude<R1 | R2, DiscordInteraction | DiscordModalSubmit | Scope>,
E1 | E2
>(pred as any, handle as any)
Exclude<R, DiscordInteraction | DiscordModalSubmit | Scope>,
E
>(pred, handle as any)

export class Autocomplete<R, E> {
readonly _tag = "Autocomplete"
constructor(
readonly predicate: (
data: Discord.ApplicationCommandDatum,
focusedOption: Discord.ApplicationCommandInteractionDataOption,
) => Effect.Effect<boolean, E, R>,
) => boolean,
readonly handle: Effect.Effect<Discord.InteractionResponse, E, R>,
) {}
}

export const autocomplete = <R1, R2, E1, E2>(
export const autocomplete = <R, E>(
pred: (
data: Discord.ApplicationCommandDatum,
focusedOption: Discord.ApplicationCommandInteractionDataOption,
) => Effect.Effect<boolean, E1, R1>,
handle: Effect.Effect<Discord.InteractionResponse, E2, R2>,
) => boolean,
handle: Effect.Effect<Discord.InteractionResponse, E, R>,
) =>
new Autocomplete<
Exclude<
R1 | R2,
R,
| DiscordInteraction
| DiscordApplicationCommand
| DiscordFocusedOption
| Scope
>,
E1 | E2
>(pred as any, handle as any)
E
>(pred, handle as any)

// ==== Command handler helpers
type DeepReadonly<T> =
T extends Array<infer R>
? ReadonlyArray<DeepReadonly<R>>
: T extends Function
? T
: T extends object
? DeepReadonlyObject<T>
: T
type DeepReadonlyObject<T> = {
readonly [P in keyof T]: DeepReadonly<T[P]>
}

export type CommandHandler<R, E, A = any> =
| Effect.Effect<Discord.InteractionResponse, E, R>
| CommandHandlerFn<R, E, A>
Expand Down
106 changes: 40 additions & 66 deletions src/Interactions/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type * as Chunk from "effect/Chunk"
import * as Option from "effect/Option"
import * as Effect from "effect/Effect"
import * as IxHelpers from "dfx/Helpers/interactions"
import * as Ctx from "dfx/Interactions/context"
Expand Down Expand Up @@ -40,91 +39,66 @@ export const handlers = <R, E, TE, A, B>(
splitDefinitions(flattened)

return {
[Discord.InteractionType.PING]: () =>
[Discord.InteractionType.PING]: _ =>
Effect.succeed({
type: Discord.InteractionCallbackType.PONG,
} as any),

[Discord.InteractionType.APPLICATION_COMMAND]: i => {
const data = i.data as Discord.ApplicationCommandDatum

return Option.match(Option.fromNullable(Commands[data.name]), {
onNone: () => Effect.fail(new DefinitionNotFound(i)),
onSome: command =>
Effect.provideService(
command.handle(i),
Ctx.ApplicationCommand,
data,
) as Handler<Exclude<R, Scope>, E, B>,
})
const command = Commands[data.name]
if (command === undefined) {
return Effect.fail(new DefinitionNotFound(i))
}
return Effect.provideService(
command.handle(i),
Ctx.ApplicationCommand,
data,
) as Handler<Exclude<R, Scope>, E, B>
},

[Discord.InteractionType.MODAL_SUBMIT]: i => {
const data = i.data as Discord.ModalSubmitDatum

return Effect.findFirst(ModalSubmit, _ =>
_.predicate(data.custom_id),
).pipe(
Effect.flatMap(
Option.match({
onNone: () => Effect.fail(new DefinitionNotFound(i)),
onSome: match =>
Effect.provideService(
match.handle(i),
Ctx.ModalSubmitData,
data,
) as Handler<R, E, B>,
}),
),
const match = ModalSubmit.find(_ => _.predicate(data.custom_id))
if (match === undefined) {
return Effect.fail(new DefinitionNotFound(i))
}
return Effect.provideService(
match.handle(i),
Ctx.ModalSubmitData,
data,
) as Handler<Exclude<R, Scope>, E, B>
},

[Discord.InteractionType.MESSAGE_COMPONENT]: i => {
const data = i.data as Discord.MessageComponentDatum

return Effect.findFirst(MessageComponent, _ =>
_.predicate(data.custom_id),
).pipe(
Effect.flatMap(
Option.match({
onNone: () => Effect.fail(new DefinitionNotFound(i)),
onSome: match =>
Effect.provideService(
match.handle(i),
Ctx.MessageComponentData,
data,
) as Handler<R, E, B>,
}),
),
const match = MessageComponent.find(_ => _.predicate(data.custom_id))
if (match === undefined) {
return Effect.fail(new DefinitionNotFound(i))
}
return Effect.provideService(
match.handle(i),
Ctx.MessageComponentData,
data,
) as Handler<Exclude<R, Scope>, E, B>
},

[Discord.InteractionType.APPLICATION_COMMAND_AUTOCOMPLETE]: i => {
const data = i.data as Discord.ApplicationCommandDatum

return Option.match(IxHelpers.focusedOption(data), {
onNone: () => Effect.fail(new DefinitionNotFound(i)),
onSome: focusedOption =>
Effect.findFirst(Autocomplete, _ =>
_.predicate(data, focusedOption),
).pipe(
Effect.flatMap(
Option.match({
onNone: () => Effect.fail(new DefinitionNotFound(i)),
onSome: match =>
Effect.provideService(
match.handle(i),
Ctx.ApplicationCommand,
data,
).pipe(
Effect.provideService(Ctx.FocusedOptionContext, {
focusedOption,
}),
) as Handler<R, E, B>,
}),
),
),
}) as Handler<Exclude<R, Scope>, E, B>
const option = IxHelpers.focusedOption(data)
if (option._tag === "None") {
return Effect.fail(new DefinitionNotFound(i))
}
const match = Autocomplete.find(_ => _.predicate(data, option.value))
if (match === undefined) {
return Effect.fail(new DefinitionNotFound(i))
}
return match
.handle(i)
.pipe(
Effect.provideService(Ctx.ApplicationCommand, data),
Effect.provideService(Ctx.FocusedOptionContext, option.value),
) as Handler<Exclude<R, Scope>, E, B>
},
}
}
12 changes: 5 additions & 7 deletions src/Interactions/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as Effect from "effect/Effect"
import type * as Discord from "dfx/types"

export { response } from "dfx/Helpers/interactions"
Expand All @@ -14,14 +13,13 @@ export {
} from "dfx/Interactions/definitions"

// Filters
export const id = (query: string) => (customId: string) =>
Effect.succeed(query === customId)
export const id = (query: string) => (customId: string) => query === customId

export const idStartsWith = (query: string) => (customId: string) =>
Effect.succeed(customId.startsWith(query))
customId.startsWith(query)

export const idRegex = (query: RegExp) => (customId: string) =>
Effect.succeed(query.test(customId))
query.test(customId)

export const option =
(command: string, optionName: string) =>
Expand All @@ -32,7 +30,7 @@ export const option =
"name"
>,
) =>
Effect.succeed(data.name === command && focusedOption.name === optionName)
data.name === command && focusedOption.name === optionName

export const optionOnly =
(optionName: string) =>
Expand All @@ -43,4 +41,4 @@ export const optionOnly =
"name"
>,
) =>
Effect.succeed(focusedOption.name === optionName)
focusedOption.name === optionName
Loading

0 comments on commit f98ef9e

Please sign in to comment.