///|
/// One typed input in a modal form.
pub struct ModalField[A] {
priv definition_ : @model.Component
priv decode_ : (ModalSubmission) -> A raise HandlerError
priv present_ : (ModalSubmission) -> Bool
}
///|
/// A composable collection of typed modal inputs.
pub struct ModalFields[A] {
priv definitions_ : Array[@model.Component]
priv decode_ : (ModalSubmission) -> A raise HandlerError
}
///|
/// What a submitted modal carries: its component tree plus the entities
/// Discord resolved for it (file uploads resolve to attachments).
priv struct ModalSubmission {
components : Array[@model.Component]
resolved : @model.ResolvedData?
}
///|
/// A typed modal definition: a route id with typed state, and typed fields.
/// The decoders are retained for the matching `on_modal` registration while
/// `show` erases them into a `ModalHandle`.
pub struct Modal[S, A] {
priv route_ : ComponentRoute[S]
priv title_ : String
priv fields_ : ModalFields[A]
}
///|
/// A type-erased modal response ready to be returned by a command or component
/// handler.
pub struct ModalHandle {
priv custom_id_ : String
priv title_ : String
priv components_ : Array[@model.Component]
}
///|
/// Why `Modal::show` could not prepare a modal response.
pub(all) suberror ModalShowError {
/// The state does not encode, or the complete custom id exceeds 100
/// characters.
InvalidCustomId(CustomIdError)
UnknownCustomId(custom_id~ : String)
NonTextInput(custom_id~ : String)
/// The value is longer, in UTF-16 units, than the input's `max_length`
/// (4000 when unset). Discord would accept it up to 4000 code points, but
/// the official clients count the field in UTF-16 units and would not
/// submit the modal until the user deleted text.
ValueTooLong(custom_id~ : String, length~ : Int32, max~ : Int32)
} derive(Debug)
///|
fn find_modal_component(
components : Array[@model.Component],
custom_id : String,
) -> @model.Component? {
for component in components {
match component {
ActionRow(row) =>
if find_modal_component(row.components, custom_id) is Some(found) {
return Some(found)
}
Label(label) =>
if find_modal_component([label.component], custom_id) is Some(found) {
return Some(found)
}
TextInput(input) if input.custom_id == custom_id => return Some(component)
StringSelect(select) if select.custom_id == custom_id =>
return Some(component)
FileUpload(upload) if upload.custom_id == custom_id =>
return Some(component)
_ => ()
}
}
None
}
///|
fn modal_component_present(
components : Array[@model.Component],
custom_id : String,
) -> Bool {
match find_modal_component(components, custom_id) {
Some(TextInput(input)) => input.value is Some(value) && !value.is_empty()
Some(StringSelect(select)) =>
select.values is Some(values) && !values.is_empty()
Some(FileUpload(upload)) =>
upload.values is Some(values) && !values.is_empty()
_ => false
}
}
///|
fn set_modal_field_optional(component : @model.Component) -> @model.Component {
match component {
Label(label) =>
Label({ ..label, component: set_modal_field_optional(label.component), })
TextInput(input) => TextInput({ ..input, required: Some(false), })
StringSelect(select) => StringSelect({ ..select, min_values: Some(0), })
FileUpload(upload) =>
FileUpload({ ..upload, required: Some(false), min_values: Some(0), })
other => other
}
}
///|
fn modal_text_input_ids(components : Array[@model.Component]) -> Array[String] {
let result = []
for component in components {
match component {
ActionRow(row) => result.append(modal_text_input_ids(row.components))
Label(label) => result.append(modal_text_input_ids([label.component]))
TextInput(input) => result.push(input.custom_id)
_ => ()
}
}
result
}
///|
fn modal_file_type_filters(
components : Array[@model.Component],
) -> Array[(String, Array[@model.FileTypeFilter])] {
let result = []
for component in components {
match component {
ActionRow(row) => result.append(modal_file_type_filters(row.components))
Label(label) => result.append(modal_file_type_filters([label.component]))
FileUpload({ custom_id, file_types: Some(filters), .. }) =>
result.push((custom_id, filters))
_ => ()
}
}
result
}
///|
fn prefill_modal_component(
component : @model.Component,
values : Map[String, String],
) -> @model.Component {
match component {
ActionRow(row) =>
ActionRow({
..row,
components: row.components.map(component => {
prefill_modal_component(component, values)
}),
})
Label(label) =>
Label({
..label,
component: prefill_modal_component(label.component, values),
})
TextInput(input) =>
match values.get(input.custom_id) {
Some(value) => TextInput({ ..input, value: Some(value), })
None => component
}
_ => component
}
}
///|
fn prefill_modal_components(
components : Array[@model.Component],
values : Map[String, String],
) -> Array[@model.Component] raise ModalShowError {
for custom_id, value in values {
match find_modal_component(components, custom_id) {
Some(TextInput(input)) => {
let max = input.max_length.unwrap_or(@model.text_input_max_length)
if value.length() > max {
raise ValueTooLong(custom_id~, length=value.length(), max~)
}
}
Some(_) => raise NonTextInput(custom_id~)
None => raise UnknownCustomId(custom_id~)
}
}
components.map(component => prefill_modal_component(component, values))
}
///|
/// Define a modern label-wrapped text input. `value` is its static prefill;
/// `Modal::show(values=...)` can override it for one response.
pub fn text_field(
custom_id~ : String,
label~ : String,
style~ : @model.TextInputStyle = Short,
description? : String,
placeholder? : String,
min_length? : Int32,
max_length? : Int32,
value? : String,
) -> ModalField[String] {
let input = @interaction.text_input(
custom_id~,
style~,
placeholder?,
min_length?,
max_length?,
required=true,
value?,
)
{
definition_: @interaction.label(label~, component=input, description?),
present_: submission => {
modal_component_present(submission.components, custom_id)
},
decode_: submission => {
match find_modal_component(submission.components, custom_id) {
Some(TextInput({ value: Some(value), .. })) if !value.is_empty() =>
value
_ => raise Malformed(reason="missing modal field: \{custom_id}")
}
},
}
}
///|
/// Define a modern label-wrapped string select.
pub fn select_field(
custom_id~ : String,
label~ : String,
options~ : Array[@model.SelectOption],
description? : String,
placeholder? : String,
min_values? : Int32,
max_values? : Int32,
) -> ModalField[Array[String]] {
let select = @interaction.string_select(
custom_id~,
options~,
placeholder?,
min_values?,
max_values?,
)
{
definition_: @interaction.label(label~, component=select, description?),
present_: submission => {
modal_component_present(submission.components, custom_id)
},
decode_: submission => {
match find_modal_component(submission.components, custom_id) {
Some(StringSelect({ values: Some(values), .. })) if !values.is_empty() =>
values
_ => raise Malformed(reason="missing modal field: \{custom_id}")
}
},
}
}
///|
/// Define a modern label-wrapped file upload. Decodes to the uploaded files'
/// attachment objects (resolved by Discord alongside the submission).
/// `file_types` filters selectable files by extension only — validate the
/// contents yourself. `min_values`/`max_values` bound the file count.
pub fn file_field(
custom_id~ : String,
label~ : String,
description? : String,
min_values? : Int32,
max_values? : Int32,
file_types? : Array[@model.FileTypeFilter],
) -> ModalField[Array[@model.Attachment]] {
let upload = @interaction.file_upload(
custom_id~,
min_values?,
max_values?,
required=true,
file_types?,
)
{
definition_: @interaction.label(label~, component=upload, description?),
present_: submission => {
modal_component_present(submission.components, custom_id)
},
decode_: submission => {
guard find_modal_component(submission.components, custom_id)
is Some(FileUpload({ values: Some(ids), .. })) &&
!ids.is_empty() else {
raise Malformed(reason="missing modal field: \{custom_id}")
}
let attachments = submission.resolved
.bind(resolved => resolved.attachments)
.unwrap_or({})
ids.map(id => {
guard attachments.get(id) is Some(attachment) else {
raise Malformed(
reason="unresolved attachment \{id} in modal field: \{custom_id}",
)
}
attachment
})
},
}
}
///|
/// Make a modal field optional. Missing and empty submissions decode to None.
pub fn[A] ModalField::optional(self : ModalField[A]) -> ModalField[A?] {
{
definition_: set_modal_field_optional(self.definition_),
present_: self.present_,
decode_: submission => {
if (self.present_)(submission) {
Some((self.decode_)(submission))
} else {
None
}
},
}
}
///|
/// Use a default when a modal field is missing or empty.
pub fn[A] ModalField::with_default(
self : ModalField[A],
value : A,
) -> ModalField[A] {
{
definition_: set_modal_field_optional(self.definition_),
present_: self.present_,
decode_: submission => {
if (self.present_)(submission) {
(self.decode_)(submission)
} else {
value
}
},
}
}
///|
/// Validate a decoded field. Returning a message rejects the submission.
pub fn[A] ModalField::validate(
self : ModalField[A],
check : (A) -> String?,
) -> ModalField[A] {
{
definition_: self.definition_,
present_: self.present_,
decode_: submission => {
let value = (self.decode_)(submission)
match check(value) {
Some(message) => raise InvalidInput(name=None, message~)
None => value
}
},
}
}
///|
/// Transform a decoded modal field without changing its emitted component.
pub fn[A, B] ModalField::map(
self : ModalField[A],
f : (A) -> B,
) -> ModalField[B] {
{
definition_: self.definition_,
present_: self.present_,
decode_: submission => f((self.decode_)(submission)),
}
}
///|
/// Lift a single field into a `ModalFields`; combine further with `zip`
/// and `map`, or use the `mapN` helpers directly.
pub fn[A] ModalFields::of(field : ModalField[A]) -> ModalFields[A] {
{ definitions_: [field.definition_], decode_: field.decode_, }
}
///|
/// Transform the decoded value without changing the emitted
/// components.
pub fn[A, B] ModalFields::map(
self : ModalFields[A],
f : (A) -> B,
) -> ModalFields[B] {
{
definitions_: self.definitions_,
decode_: submission => f((self.decode_)(submission)),
}
}
///|
/// Concatenate two field sets and pair their decoded values.
pub fn[A, B] ModalFields::zip(
self : ModalFields[A],
other : ModalFields[B],
) -> ModalFields[(A, B)] {
let definitions = self.definitions_.copy()
definitions.append(other.definitions_)
{
definitions_: definitions,
decode_: submission => {
((self.decode_)(submission), (other.decode_)(submission))
},
}
}
///|
/// Build a modal's field set from one field, mapping its decoded
/// value into the handler's argument type.
pub fn[A, Z] ModalFields::map1(
a : ModalField[A],
f : (A) -> Z,
) -> ModalFields[Z] {
ModalFields::of(a).map(f)
}
///|
/// Build a modal's field set from two fields, combining their
/// decoded values with the final function argument.
pub fn[A, B, Z] ModalFields::map2(
a : ModalField[A],
b : ModalField[B],
f : (A, B) -> Z,
) -> ModalFields[Z] {
ModalFields::of(a).zip(ModalFields::of(b)).map(pair => f(pair.0, pair.1))
}
///|
/// Build a modal's field set from three fields, combining their
/// decoded values with the final function argument.
pub fn[A, B, C, Z] ModalFields::map3(
a : ModalField[A],
b : ModalField[B],
c : ModalField[C],
f : (A, B, C) -> Z,
) -> ModalFields[Z] {
ModalFields::map2(a, b, (a, b) => (a, b))
.zip(ModalFields::of(c))
.map(value => f(value.0.0, value.0.1, value.1))
}
///|
/// Build a modal's field set from four fields, combining their
/// decoded values with the final function argument.
pub fn[A, B, C, D, Z] ModalFields::map4(
a : ModalField[A],
b : ModalField[B],
c : ModalField[C],
d : ModalField[D],
f : (A, B, C, D) -> Z,
) -> ModalFields[Z] {
ModalFields::map3(a, b, c, (a, b, c) => (a, b, c))
.zip(ModalFields::of(d))
.map(value => f(value.0.0, value.0.1, value.0.2, value.1))
}
///|
/// Build a modal's field set from five fields, combining their
/// decoded values with the final function argument.
pub fn[A, B, C, D, E, Z] ModalFields::map5(
a : ModalField[A],
b : ModalField[B],
c : ModalField[C],
d : ModalField[D],
e : ModalField[E],
f : (A, B, C, D, E) -> Z,
) -> ModalFields[Z] {
ModalFields::map4(a, b, c, d, (a, b, c, d) => (a, b, c, d))
.zip(ModalFields::of(e))
.map(value => f(value.0.0, value.0.1, value.0.2, value.0.3, value.1))
}
///|
/// Define a typed modal. Like `component_route`, `state` encodes the value
/// carried in the custom id after `custom_id:`; pass `CustomIdCodec::unit()`
/// for a modal without state. Handlers receive the decoded state and form.
///
/// ```mbt check
/// test "construct and validate a typed modal" {
/// let feedback = @app.modal(
/// custom_id="feedback",
/// title="Feedback",
/// state=@app.CustomIdCodec::int(),
/// fields=@app.ModalFields::of(
/// @app.text_field(custom_id="details", label="Details").optional(),
/// ),
/// )
/// let app = @app.AppBuilder()
/// app.on_modal(feedback, Raw(_ => ()))
/// app.build() |> ignore
/// let _ : @app.ModalHandle = feedback.show(42)
/// }
/// ```
pub fn[S, A] modal(
custom_id~ : String,
title~ : String,
state~ : CustomIdCodec[S],
fields~ : ModalFields[A],
) -> Modal[S, A] {
{
route_: component_route(id=custom_id, state~),
title_: title,
fields_: fields,
}
}
///|
/// Prepare a modal response carrying `state`, optionally overriding
/// text-input defaults by `custom_id` for this response only. Raises
/// `ModalShowError::InvalidCustomId` when the state does not encode or the
/// complete id exceeds 100 characters, and the other variants when an
/// override key is unknown, does not name a text input, or carries a value
/// longer than the input's `max_length` (4000 when unset) in UTF-16 units.
pub fn[S, A] Modal::show(
self : Modal[S, A],
state : S,
values? : Map[String, String],
) -> ModalHandle raise ModalShowError {
let custom_id = self.route_.custom_id(state) catch {
error => raise InvalidCustomId(error)
}
let components = match values {
Some(overrides) =>
prefill_modal_components(self.fields_.definitions_, overrides)
None => self.fields_.definitions_.copy()
}
{ custom_id_: custom_id, title_: self.title_, components_: components, }
}
///|
fn[S, A] Modal::decode(
self : Modal[S, A],
components : Array[@model.Component],
resolved? : @model.ResolvedData,
) -> A raise HandlerError {
(self.fields_.decode_)({ components, resolved, })
}
///|
async fn ModalHandle::send_command(
self : ModalHandle,
ctx : @framework.CommandCtx,
) -> Unit {
ctx.show_modal(
custom_id=self.custom_id_,
title=self.title_,
components=self.components_,
)
}
///|
async fn ModalHandle::send_component(
self : ModalHandle,
ctx : @framework.ComponentCtx,
) -> Unit {
ctx.show_modal(
custom_id=self.custom_id_,
title=self.title_,
components=self.components_,
)
}
///|
/// Initial response returned by an immediate modal handler.
pub(all) enum ModalReply {
/// Edit the message whose component opened the modal. A modal opened by a
/// command has no such message; replying this way raises
/// `HandlerError::UnsupportedResponse` before anything is sent.
UpdateMessage(MessageUpdate)
Message(InitialResponse)
}
///|
/// Reply with a new message; set `ephemeral=true` to show it only to the
/// submitting user.
pub fn ModalReply::message(
content? : String,
embeds? : Array[@model.Embed],
components? : Array[@model.Component],
files? : Array[@dhttp.FileUpload],
allowed_mentions? : @model.AllowedMentions,
poll? : @model.PollCreateRequest,
ephemeral~ : Bool = false,
) -> ModalReply {
Message(
InitialResponse::message(
content?,
embeds?,
components?,
files?,
allowed_mentions?,
poll?,
ephemeral~,
),
)
}
///|
/// Reply by editing the message whose component opened the modal. Omitted
/// fields keep their current value (see `MessageUpdate`).
pub fn ModalReply::update_message(
content? : String,
clear_content~ : Bool = false,
embeds? : Array[@model.Embed],
components? : Array[@model.Component],
allowed_mentions? : @model.AllowedMentions,
files? : Array[@dhttp.FileUpload],
keep_attachments? : Array[@model.AttachmentRequest],
poll? : @model.PollCreateRequest,
) -> ModalReply {
UpdateMessage(
MessageUpdate(
content?,
clear_content~,
embeds?,
components?,
allowed_mentions?,
files?,
keep_attachments?,
poll?,
),
)
}
///|
/// Execution strategy for a typed modal submission. Handlers receive the
/// decoded state and form.
pub(all) enum ModalSubmitHandler[S, A] {
Immediate(async (ModalImmediateCtx, S, A) -> ModalReply)
DeferredMessage(ephemeral~ : Bool, async (ModalDeferredCtx, S, A) -> Unit)
/// Acknowledge without changing the message whose component opened the
/// modal, then run; `edit_original` edits that message. A modal opened by a
/// command has no such message, so the submission fails with
/// `HandlerError::UnsupportedResponse` before anything is sent.
DeferredUpdate(async (ModalDeferredCtx, S, A) -> Unit)
Raw(async (@framework.ModalCtx) -> Unit)
}
///|
/// How this submission is acknowledged. An update of a modal opened by a
/// command is never deferred: it fails before the handler runs, and the
/// policy answers with an initial message.
fn[S, A] ModalSubmitHandler::ack_plan(
self : ModalSubmitHandler[S, A],
raw : @framework.ModalCtx,
) -> AckPlan {
match self {
DeferredMessage(ephemeral~, _) => DeferMessage(ephemeral~)
DeferredUpdate(_) if raw.origin() is FromComponent(_) => DeferUpdate
DeferredUpdate(_) | Immediate(_) | Raw(_) => Respond
}
}
///|
/// Reject a submission this handler cannot answer: an update of a modal that
/// a command opened.
fn[S, A] ModalSubmitHandler::preflight(
self : ModalSubmitHandler[S, A],
raw : @framework.ModalCtx,
) -> Unit raise HandlerError {
if self is DeferredUpdate(_) {
require_component_origin(raw)
}
}
///|
fn require_component_origin(
raw : @framework.ModalCtx,
) -> Unit raise HandlerError {
guard raw.origin() is FromComponent(_) else {
raise UnsupportedResponse(
reason="modal \{raw.data.custom_id} was opened by a command, so there is no message to update",
)
}
}
///|
/// Read-only modal-submit context for handlers returning an initial message.
pub struct ModalImmediateCtx {
priv raw_ : @framework.ModalCtx
priv bot_ : AppCtx
}
///|
/// Modal-submit context available after a deferred response.
pub struct ModalDeferredCtx {
priv raw_ : @framework.ModalCtx
priv bot_ : AppCtx
priv waiter_ : ComponentWaiter?
priv warn_ : (AppWarning) -> Unit
}
///|
/// The app-level services: the REST client and application id.
pub fn ModalImmediateCtx::app(self : ModalImmediateCtx) -> AppCtx {
self.bot_
}
///|
/// Guild or DM invocation scope, carrying the invoking member or user.
pub fn ModalImmediateCtx::scope(
self : ModalImmediateCtx,
) -> @framework.InvocationScope {
self.raw_.scope()
}
///|
/// The validated guild invocation. In a DM this raises
/// `HandlerError::GuildOnly`, which the error policy renders normally.
pub fn ModalImmediateCtx::guild_scope(
self : ModalImmediateCtx,
) -> @framework.GuildInvocation raise HandlerError {
require_guild(self.raw_.guild_scope())
}
///|
/// The invoking user.
pub fn ModalImmediateCtx::user(self : ModalImmediateCtx) -> @model.User {
self.raw_.user()
}
///|
/// What opened the modal: a component (with its host message) or a
/// command.
pub fn ModalImmediateCtx::origin(
self : ModalImmediateCtx,
) -> @framework.ModalOrigin {
self.raw_.origin()
}
///|
/// The full interaction payload.
pub fn ModalImmediateCtx::interaction(
self : ModalImmediateCtx,
) -> @model.Interaction {
self.raw_.interaction
}
///|
/// The guild the interaction was invoked in, or `None` outside guilds. Use
/// `guild_scope()` for flows that require a guild; this accessor is for
/// maybe-guild flows where DMs are valid.
pub fn ModalImmediateCtx::guild_id(self : ModalImmediateCtx) -> @model.GuildId? {
self.raw_.interaction.guild_id
}
///|
/// Milliseconds left to return the initial response, or `None` when the
/// executor set no deadline.
pub fn ModalImmediateCtx::remaining_ms(self : ModalImmediateCtx) -> Int32? {
self.raw_.remaining_ms()
}
///|
/// Escape hatch to the underlying context. Its response methods bypass this
/// handler's discipline of returning its initial response: a callback sent
/// through it makes the returned reply fail with
/// `ResponseGateError::AlreadyResponded`, which reaches the error policy.
/// Prefer the read-only accessors on this context. Each use raises an
/// `alert_unchecked` warning; once the bypass is deliberate, silence it with
/// `warnings = "-alert_unchecked"` in that package's `moon.pkg`.
#alert(unchecked, "bypasses the initial-response discipline; a reply sent through it makes the handler's returned reply fail with AlreadyResponded")
pub fn ModalImmediateCtx::unchecked_raw(
self : ModalImmediateCtx,
) -> @framework.ModalCtx {
self.raw_
}
///|
/// The app-level services: the REST client and application id.
pub fn ModalDeferredCtx::app(self : ModalDeferredCtx) -> AppCtx {
self.bot_
}
///|
/// Guild or DM invocation scope, carrying the invoking member or user.
pub fn ModalDeferredCtx::scope(
self : ModalDeferredCtx,
) -> @framework.InvocationScope {
self.raw_.scope()
}
///|
/// The validated guild invocation. In a DM this raises
/// `HandlerError::GuildOnly`, which the error policy renders normally.
pub fn ModalDeferredCtx::guild_scope(
self : ModalDeferredCtx,
) -> @framework.GuildInvocation raise HandlerError {
require_guild(self.raw_.guild_scope())
}
///|
/// The invoking user.
pub fn ModalDeferredCtx::user(self : ModalDeferredCtx) -> @model.User {
self.raw_.user()
}
///|
/// What opened the modal: a component (with its host message) or a
/// command.
pub fn ModalDeferredCtx::origin(
self : ModalDeferredCtx,
) -> @framework.ModalOrigin {
self.raw_.origin()
}
///|
/// The full interaction payload.
pub fn ModalDeferredCtx::interaction(
self : ModalDeferredCtx,
) -> @model.Interaction {
self.raw_.interaction
}
///|
/// The guild the interaction was invoked in, or `None` outside guilds. Use
/// `guild_scope()` for flows that require a guild; this accessor is for
/// maybe-guild flows where DMs are valid.
pub fn ModalDeferredCtx::guild_id(self : ModalDeferredCtx) -> @model.GuildId? {
self.raw_.interaction.guild_id
}
///|
/// Escape hatch for advanced inspection. Calling response methods on the
/// raw value opts out of this wrapper's response discipline.
/// Failure handling still follows the gate's real response state.
pub fn ModalDeferredCtx::raw(self : ModalDeferredCtx) -> @framework.ModalCtx {
self.raw_
}
///|
/// Fetch the original response.
pub async fn ModalDeferredCtx::original(
self : ModalDeferredCtx,
) -> @model.Message {
self.raw_.original_response()
}
///|
/// Edit the original response after deferring. New `files` replace
/// the existing attachments unless `keep_attachments` lists the ones to retain;
/// `poll` can be added only here, while editing the deferred response.
pub async fn ModalDeferredCtx::edit_original(
self : ModalDeferredCtx,
content? : String,
clear_content~ : Bool = false,
embeds? : Array[@model.Embed],
components? : Array[@model.Component],
allowed_mentions? : @model.AllowedMentions,
files? : Array[@dhttp.FileUpload],
keep_attachments? : Array[@model.AttachmentRequest],
poll? : @model.PollCreateRequest,
) -> @model.Message {
self.raw_.edit_response(
content?,
clear_content~,
embeds?,
components?,
allowed_mentions?,
files?,
keep_attachments?,
poll?,
)
}
///|
/// Delete the original response.
pub async fn ModalDeferredCtx::delete_original(self : ModalDeferredCtx) -> Unit {
self.raw_.delete_response()
}
///|
/// Send a followup message after the initial deferred response.
pub async fn ModalDeferredCtx::followup(
self : ModalDeferredCtx,
content? : String,
embeds? : Array[@model.Embed],
components? : Array[@model.Component],
files? : Array[@dhttp.FileUpload],
allowed_mentions? : @model.AllowedMentions,
poll? : @model.PollCreateRequest,
ephemeral~ : Bool = false,
) -> @model.Message {
self.raw_.followup(
content?,
embeds?,
components?,
files?,
allowed_mentions?,
poll?,
ephemeral~,
)
}
///|
/// Fetch a followup message sent for this interaction.
pub async fn ModalDeferredCtx::get_followup(
self : ModalDeferredCtx,
message_id : @model.MessageId,
) -> @model.Message {
self.raw_.get_followup(message_id)
}
///|
/// Edit a followup message sent for this interaction. New `files` replace
/// its attachments unless `keep_attachments` lists the ones to retain.
pub async fn ModalDeferredCtx::edit_followup(
self : ModalDeferredCtx,
message_id : @model.MessageId,
content? : String,
clear_content~ : Bool = false,
embeds? : Array[@model.Embed],
components? : Array[@model.Component],
allowed_mentions? : @model.AllowedMentions,
files? : Array[@dhttp.FileUpload],
keep_attachments? : Array[@model.AttachmentRequest],
) -> @model.Message {
self.raw_.edit_followup(
message_id,
content?,
clear_content~,
embeds?,
components?,
allowed_mentions?,
files?,
keep_attachments?,
)
}
///|
/// Delete a followup message sent for this interaction.
pub async fn ModalDeferredCtx::delete_followup(
self : ModalDeferredCtx,
message_id : @model.MessageId,
) -> Unit {
self.raw_.delete_followup(message_id)
}
///|
async fn InitialResponse::send_modal(
self : InitialResponse,
ctx : @framework.ModalCtx,
) -> Unit {
ctx.respond(
content?=self.content_,
embeds?=self.embeds_,
components?=self.components_,
files?=self.files_,
allowed_mentions?=self.allowed_mentions_,
poll?=self.poll_,
ephemeral=self.ephemeral_,
)
}
///|
/// Wait for the next click on the component `route.custom_id(state)`
/// identifies — build the button with the same route and state. By default
/// only the invoking user may satisfy the wait; pass `from=Anyone` for a
/// public flow or `from=User(id)` for someone else. Returns `None` on
/// timeout, or at once without a gateway connection. Raises `CustomIdError`
/// when the state does not encode.
///
/// The returned context has not been answered, and the waited interaction does
/// not pass through admission or the automatic defer: respond to it (for
/// example `update_message` or `defer_update`) within three seconds.
pub async fn[A] ModalDeferredCtx::wait_for_component(
self : ModalDeferredCtx,
route : ComponentRoute[A],
state : A,
from~ : WaitFrom = Invoker,
timeout_ms? : Int32,
) -> @framework.ComponentCtx? {
wait_for_route(
self.waiter_,
self.warn_,
route,
state,
from.user_filter(self.user().id),
timeout_ms,
)
}
///|
async fn MessageUpdate::send_modal(
self : MessageUpdate,
ctx : @framework.ModalCtx,
) -> Unit {
require_component_origin(ctx)
ctx.update_message(
content?=self.content_,
clear_content=self.clear_content_,
embeds?=self.embeds_,
components?=self.components_,
allowed_mentions?=self.allowed_mentions_,
files?=self.files_,
keep_attachments?=self.keep_attachments_,
poll?=self.poll_,
)
}
///|
async fn ModalReply::send(self : ModalReply, ctx : @framework.ModalCtx) -> Unit {
match self {
UpdateMessage(update) => update.send_modal(ctx)
Message(response) => response.send_modal(ctx)
}
}
///|
async fn[S, A] dispatch_modal_handler(
modal : Modal[S, A],
handler : ModalSubmitHandler[S, A],
bot : AppCtx,
waiter : ComponentWaiter?,
warn : (AppWarning) -> Unit,
raw : @framework.ModalCtx,
) -> Unit {
let decode = () => {
let state = modal.route_.decode(raw.data.custom_id)
let form = modal.decode(raw.data.components, resolved?=raw.data.resolved)
(state, form)
}
match handler {
Raw(run) => run(raw)
Immediate(run) => {
let (state, form) = decode()
run({ raw_: raw, bot_: bot, }, state, form).send(raw)
}
DeferredMessage(ephemeral~, run) => {
let (state, form) = decode()
ensure_acknowledged(modal_admission(raw), DeferMessage(ephemeral~))
run({ raw_: raw, bot_: bot, waiter_: waiter, warn_: warn, }, state, form)
}
DeferredUpdate(run) => {
require_component_origin(raw)
let (state, form) = decode()
ensure_acknowledged(modal_admission(raw), DeferUpdate)
run({ raw_: raw, bot_: bot, waiter_: waiter, warn_: warn, }, state, form)
}
}
}