///|
/// The operation whose handler failed.
#non_exhaustive
pub(all) enum FailureOrigin {
Command(name~ : String)
Autocomplete(name~ : String)
Component(custom_id~ : String)
Modal(custom_id~ : String)
Event(kind~ : @model.EventKind)
Service(name~ : String)
} derive(Debug)
///|
/// The underlying interaction context for a failure.
///
/// Event, service, and autocomplete failures have no raw context. Responding
/// through these contexts without checking the response state may fail; prefer
/// `FailureCtx::respond_error()` for ordinary error responses.
pub(all) enum FailureRaw {
RawCommand(@framework.CommandCtx)
RawComponent(@framework.ComponentCtx)
RawModal(@framework.ModalCtx)
}
///|
/// Context handed to the error policy when a handler fails.
pub struct FailureCtx {
priv origin_ : FailureOrigin
priv raw_ : FailureRaw?
priv warn_ : (AppWarning) -> Unit
}
///|
/// The operation whose handler failed.
pub fn FailureCtx::origin(self : FailureCtx) -> FailureOrigin {
self.origin_
}
///|
/// Read the gate's current state. Event, service, and autocomplete failures
/// have no raw context and return `None`.
pub fn FailureCtx::response_state(
self : FailureCtx,
) -> @framework.ResponseState? {
match self.raw_ {
Some(RawCommand(ctx)) => Some(ctx.response_state())
Some(RawComponent(ctx)) => Some(ctx.response_state())
Some(RawModal(ctx)) => Some(ctx.response_state())
None => None
}
}
///|
/// Access the underlying interaction context. Event, service, and autocomplete failures
/// return `None`. Direct responses through this escape hatch may fail unless
/// the response state is checked first; normally use `respond_error()`.
pub fn FailureCtx::raw(self : FailureCtx) -> FailureRaw? {
self.raw_
}
///|
/// The interaction payload associated with this failure. Event and service
/// failures return `None`.
pub fn FailureCtx::interaction(self : FailureCtx) -> @model.Interaction? {
match self.raw_ {
Some(RawCommand(ctx)) => Some(ctx.interaction)
Some(RawComponent(ctx)) => Some(ctx.interaction)
Some(RawModal(ctx)) => Some(ctx.interaction)
None => None
}
}
///|
/// The user who invoked the failed interaction. Event and service failures
/// return `None`.
pub fn FailureCtx::user(self : FailureCtx) -> @model.User? {
guard self.interaction() is Some(interaction) else { return None }
match interaction.guild_member {
Some(guild_member) =>
match guild_member.user {
Some(user) => Some(user)
None => interaction.user
}
None => interaction.user
}
}
///|
fn error_response_summary(
content : String?,
embeds : Array[@model.Embed]?,
components : Array[@model.Component]?,
files : Array[@dhttp.FileUpload]?,
) -> String {
guard content is None else { return content.unwrap() }
let kinds : Array[String] = []
if embeds is Some(_) {
kinds.push("embeds")
}
if components is Some(_) {
kinds.push("components")
}
if files is Some(_) {
kinds.push("files")
}
match kinds {
[] => "empty error response"
[kind] => "\{kind}-only error response"
_ => "\{kinds.join("+")} error response"
}
}
///|
/// Send an error without violating Discord's one-initial-response rule.
/// A pending callback gets an initial response; a deferred message whose
/// loading placeholder is still showing is edited in place (keeping the
/// visibility chosen when deferring); any other sent or unconfirmed callback
/// gets a followup. Expired callbacks and failures without a raw context
/// send a payload summary to the warning hook.
pub async fn FailureCtx::respond_error(
self : FailureCtx,
content? : String,
embeds? : Array[@model.Embed],
components? : Array[@model.Component],
files? : Array[@dhttp.FileUpload],
allowed_mentions? : @model.AllowedMentions,
ephemeral~ : Bool = true,
) -> Unit {
guard self.raw_ is Some(raw) else {
(self.warn_)(
ErrorResponseWithoutInteraction(
summary=error_response_summary(content, embeds, components, files),
),
)
return
}
let replied = try {
match raw {
RawCommand(ctx) =>
ctx.reply(
content?,
embeds?,
components?,
files?,
allowed_mentions?,
ephemeral~,
)
RawComponent(ctx) =>
ctx.reply(
content?,
embeds?,
components?,
files?,
allowed_mentions?,
ephemeral~,
)
RawModal(ctx) =>
ctx.reply(
content?,
embeds?,
components?,
files?,
allowed_mentions?,
ephemeral~,
)
}
true
} catch {
@framework.ResponseGateError::Expired => false
error => raise error
}
if !replied {
(self.warn_)(
ErrorResponseExpired(
summary=error_response_summary(content, embeds, components, files),
),
)
}
}
///|
/// Policy invoked for interaction, event, and service failures.
pub type ErrorPolicy = async (FailureCtx, Error) -> Unit
///|
/// Tell the user an interaction failed without echoing internal details.
/// Event, service, and autocomplete failures have nobody to tell.
async fn respond_if_interactive(ctx : FailureCtx, message : String) -> Unit {
if ctx.raw_ is Some(_) {
ctx.respond_error(content=message)
}
}
///|
/// How the default error policy answers a failure.
priv enum DefaultReply {
/// Tell the user what went wrong.
Tell(String, ephemeral~ : Bool)
/// Report a warning, then answer an interactive failure generically.
Report(AppWarning, String)
}
///|
fn default_reply(error : Error, origin : FailureOrigin) -> DefaultReply {
match error {
HandlerError::UserMessage(message~, ephemeral~) => Tell(message, ephemeral~)
HandlerError::GuildOnly =>
Tell("This command can only be used in a server.", ephemeral=true)
HandlerError::DmOnly =>
Tell("This command can only be used in a direct message.", ephemeral=true)
HandlerError::CheckFailed =>
Tell("You cannot use this command.", ephemeral=true)
HandlerError::MissingPermission(permissions) =>
Tell("Missing required permission: \{Repr(permissions)}", ephemeral=true)
HandlerError::OnCooldown(retry_after_ms~) =>
Tell(
"This command is on cooldown. Try again in \{retry_after_ms} ms.",
ephemeral=true,
)
HandlerError::InvalidInput(name=Some(name), message~) =>
Tell("Invalid value for \{name}: \{message}", ephemeral=true)
HandlerError::InvalidInput(name=None, message~) =>
Tell("Invalid input: \{message}", ephemeral=true)
HandlerError::Busy =>
Tell(
"The bot is busy right now. Please try again in a moment.",
ephemeral=true,
)
HandlerError::Malformed(reason~) =>
Report(
MalformedInteraction(origin~, reason~),
"This interaction is out of date. Please try again.",
)
_ => Report(UnhandledFailure(origin~, error~), "Something went wrong.")
}
}
///|
fn default_error_policy(warn : (AppWarning) -> Unit) -> ErrorPolicy {
(ctx, error) => {
match default_reply(error, ctx.origin()) {
Tell(content, ephemeral~) => ctx.respond_error(content~, ephemeral~)
Report(warning, message) => {
warn(warning)
respond_if_interactive(ctx, message)
}
}
}
}
///|
async fn invoke_error_policy(
policy : ErrorPolicy,
ctx : FailureCtx,
error : Error,
) -> Unit {
policy(ctx, error) catch {
policy_error if @async.is_being_cancelled() => raise policy_error
policy_error => (ctx.warn_)(ErrorPolicyFailed(error=policy_error))
}
}