Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move value limitations away from type_setter #319

Open
wants to merge 10 commits into
base: next
Choose a base branch
from
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:

include:
- name: MSRV
toolchain: 1.74.0
toolchain: 1.80.0
# don't do doctests because they rely on new features for brevity
# copy known Cargo.lock to avoid random dependency MSRV bumps to mess up our test
command: cp .github/Cargo-msrv.lock Cargo.lock && cargo test --all-features --lib --tests
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ authors = ["kangalio <[email protected]>"]
edition = "2021"
name = "poise"
version = "0.6.1"
rust-version = "1.74.0"
rust-version = "1.80.0"
description = "A Discord bot framework for serenity"
license = "MIT"
repository = "https://github.com/serenity-rs/poise/"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![Docs](https://img.shields.io/badge/docs-online-informational)](https://docs.rs/poise/)
[![Docs (git)](https://img.shields.io/badge/docs%20%28git%29-online-informational)](https://serenity-rs.github.io/poise/)
[![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Rust: 1.74+](https://img.shields.io/badge/rust-1.74+-93450a)](https://blog.rust-lang.org/2023/11/16/Rust-1.74.0.html)
[![Rust: 1.80+](https://img.shields.io/badge/rust-1.80+-93450a)](https://blog.rust-lang.org/2024/07/25/Rust-1.80.0.html)

# Poise
Poise is an opinionated Discord bot framework with a few distinctive features:
Expand Down
3 changes: 1 addition & 2 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ by poise.

# basic_structure

Showcases the basics of poise: `FrameworkOptions`, creating and accessing the data struct, a help
command, defining commands and sending responses.
Showcases the basics of poise: `FrameworkOptions`, creating and accessing the data struct, defining commands and sending responses.

# feature_showcase

Expand Down
20 changes: 0 additions & 20 deletions examples/basic_structure/commands.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,5 @@
use crate::{Context, Error};

/// Show this help menu
#[poise::command(prefix_command, track_edits, slash_command)]
pub async fn help(
ctx: Context<'_>,
#[description = "Specific command to show help about"]
#[autocomplete = "poise::builtins::autocomplete_command"]
command: Option<String>,
) -> Result<(), Error> {
poise::builtins::help(
ctx,
command.as_deref(),
poise::builtins::HelpConfiguration {
extra_text_at_bottom: "This is an example bot made to showcase features of my custom Discord bot framework",
..Default::default()
},
)
.await?;
Ok(())
}

/// Vote for something
///
/// Enter `~vote pumpkin` to vote for pumpkins
Expand Down
4 changes: 2 additions & 2 deletions examples/basic_structure/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ async fn main() {
// FrameworkOptions contains all of poise's configuration option in one struct
// Every option can be omitted to use its default value
let options = poise::FrameworkOptions {
commands: vec![commands::help(), commands::vote(), commands::getvotes()],
commands: vec![commands::vote(), commands::getvotes()],
prefix_options: poise::PrefixFrameworkOptions {
prefix: Some("~".into()),
edit_tracker: Some(Arc::new(poise::EditTracker::for_timespan(
Expand Down Expand Up @@ -81,7 +81,7 @@ async fn main() {
// Enforce command checks even for owners (enforced by default)
// Set to true to bypass checks, which is useful for testing
skip_checks_for_owners: false,
event_handler: |_ctx, event, _framework, _data| {
event_handler: |_framework, event| {
Box::pin(async move {
println!(
"Got an event in event handler: {:?}",
Expand Down
11 changes: 5 additions & 6 deletions examples/event_handler/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@ async fn main() {
})
})
.options(poise::FrameworkOptions {
event_handler: |ctx, event, framework, data| {
Box::pin(event_handler(ctx, event, framework, data))
},
event_handler: |framework, event| Box::pin(event_handler(framework, event)),
..Default::default()
})
.build();
Expand All @@ -46,11 +44,12 @@ async fn main() {
}

async fn event_handler(
ctx: &serenity::Context,
framework: poise::FrameworkContext<'_, Data, Error>,
event: &serenity::FullEvent,
_framework: poise::FrameworkContext<'_, Data, Error>,
data: &Data,
) -> Result<(), Error> {
let data = framework.user_data;
let ctx = framework.serenity_context;

match event {
serenity::FullEvent::Ready { data_about_bot, .. } => {
println!("Logged in as {}", data_about_bot.user.name);
Expand Down
35 changes: 14 additions & 21 deletions examples/feature_showcase/autocomplete.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use futures::{Stream, StreamExt};
use std::fmt::Write as _;

use poise::serenity_prelude as serenity;
Expand All @@ -11,40 +10,34 @@ use crate::{Context, Error};
// The first parameter of that function is ApplicationContext or Context, and the second parameter
// is a &str of the partial input which the user has typed so far.
//
// As the return value of autocomplete functions, you can return a Stream, an Iterator, or an
// IntoIterator like Vec<T> and [T; N].
//
// The returned collection type must be a &str/String (or number, if you're implementing
// autocomplete on a number type). Wrap the type in serenity::AutocompleteChoice to set a custom label
// for each choice which will be displayed in the Discord UI.
//
// Example function return types (assuming non-number parameter -> autocomplete choices are string):
// - `-> impl Stream<String>`
// - `-> Vec<String>`
// - `-> impl Iterator<String>`
// - `-> impl Iterator<&str>`
// - `-> impl Iterator<serenity::AutocompleteChoice>
// As the return value of autocomplete functions, you must return `serenity::CreateAutocompleteResponse`.

async fn autocomplete_name<'a>(
_ctx: Context<'_>,
partial: &'a str,
) -> impl Stream<Item = String> + 'a {
futures::stream::iter(&["Amanda", "Bob", "Christian", "Danny", "Ester", "Falk"])
.filter(move |name| futures::future::ready(name.starts_with(partial)))
.map(|name| name.to_string())
) -> serenity::CreateAutocompleteResponse {
let choices = ["Amanda", "Bob", "Christian", "Danny", "Ester", "Falk"]
.into_iter()
.filter(move |name| name.starts_with(partial))
.map(serenity::AutocompleteChoice::from)
.collect();

serenity::CreateAutocompleteResponse::new().set_choices(choices)
}

async fn autocomplete_number(
_ctx: Context<'_>,
_partial: &str,
) -> impl Iterator<Item = serenity::AutocompleteChoice> {
) -> serenity::CreateAutocompleteResponse {
// Dummy choices
[1_u32, 2, 3, 4, 5].iter().map(|&n| {
let choices = [1_u32, 2, 3, 4, 5].iter().map(|&n| {
serenity::AutocompleteChoice::new(
format!("{n} (why did discord even give autocomplete choices separate labels)"),
n,
)
})
});

serenity::CreateAutocompleteResponse::new().set_choices(choices.collect())
}

/// Greet a user. Showcasing autocomplete!
Expand Down
20 changes: 0 additions & 20 deletions examples/feature_showcase/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,3 @@ pub async fn servers(ctx: Context<'_>) -> Result<(), Error> {
poise::builtins::servers(ctx).await?;
Ok(())
}

#[poise::command(slash_command, prefix_command)]
pub async fn help(ctx: Context<'_>, command: Option<String>) -> Result<(), Error> {
let configuration = poise::builtins::HelpConfiguration {
// [configure aspects about the help message here]
..Default::default()
};
poise::builtins::help(ctx, command.as_deref(), configuration).await?;
Ok(())
}

#[poise::command(slash_command, prefix_command)]
pub async fn pretty_help(ctx: Context<'_>, command: Option<String>) -> Result<(), Error> {
let configuration = poise::builtins::PrettyHelpConfiguration {
// [configure aspects about the help message here]
..Default::default()
};
poise::builtins::pretty_help(ctx, command.as_deref(), configuration).await?;
Ok(())
}
2 changes: 1 addition & 1 deletion examples/feature_showcase/choice_parameter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub async fn choice(
//
// Limitations: due to macro limitations (partially self-imposed, partially external), poise
// currently does not support Options parameters, and only supports parameter types that can be
// constructed from a literal (https://doc.rust-lang.org/reference/expressions/literal-expr.html).
// constructed from a literal which can be converted to a string at compile time.

#[poise::command(slash_command)]
pub async fn inline_choice(
Expand Down
4 changes: 1 addition & 3 deletions examples/feature_showcase/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ async fn main() {
bool_parameter::oracle(),
#[cfg(feature = "cache")]
builtins::servers(),
builtins::help(),
builtins::pretty_help(),
checks::shutdown(),
checks::modonly(),
checks::delete(),
Expand Down Expand Up @@ -84,7 +82,7 @@ async fn main() {
],
prefix_options: poise::PrefixFrameworkOptions {
prefix: Some("~".into()),
non_command_message: Some(|_, _, msg| {
non_command_message: Some(|_, msg| {
Box::pin(async move {
println!("non command message!: {}", msg.content);
Ok(())
Expand Down
6 changes: 4 additions & 2 deletions examples/feature_showcase/modal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@ pub async fn component_modal(ctx: crate::Context<'_>) -> Result<(), Error> {

ctx.send(reply).await?;

while let Some(mci) = serenity::ComponentInteractionCollector::new(ctx.serenity_context())
let serenity_ctx = ctx.serenity_context();
while let Some(mci) = serenity::ComponentInteractionCollector::new(serenity_ctx)
.timeout(std::time::Duration::from_secs(120))
.filter(move |mci| mci.data.custom_id == "open_modal")
.await
{
let data =
poise::execute_modal_on_component_interaction::<MyModal>(ctx, mci, None, None).await?;
poise::execute_modal_on_component_interaction::<MyModal>(serenity_ctx, mci, None, None)
.await?;
println!("Got data: {:?}", data);
}
Ok(())
Expand Down
9 changes: 7 additions & 2 deletions examples/fluent_localization/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use poise::serenity_prelude as serenity;
use translation::tr;

pub struct Data {
translations: translation::Translations,
translations: &'static translation::Translations,
}

type Error = Box<dyn std::error::Error + Send + Sync>;
Expand Down Expand Up @@ -62,7 +62,12 @@ async fn main() {

let mut commands = vec![welcome(), info(), register()];
let translations = translation::read_ftl().expect("failed to read translation files");
translation::apply_translations(&translations, &mut commands);

// We leak the translations so we can easily copy around `&'static str`s, to the downside
// that the OS will reclaim the memory at the end of `main` instead of the Drop implementation.
let translations: &'static translation::Translations = Box::leak(Box::new(translations));

translation::apply_translations(translations, &mut commands);

let token = std::env::var("TOKEN").unwrap();
let intents = serenity::GatewayIntents::non_privileged();
Expand Down
45 changes: 25 additions & 20 deletions examples/fluent_localization/translation.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! Wraps the fluent API and provides easy to use functions and macros for translation

use std::borrow::Cow;

use crate::{Context, Data, Error};

type FluentBundle = fluent::bundle::FluentBundle<
Expand Down Expand Up @@ -30,27 +32,27 @@ pub(crate) use tr;

/// Given a language file and message identifier, returns the translation
pub fn format(
bundle: &FluentBundle,
bundle: &'static FluentBundle,
id: &str,
attr: Option<&str>,
args: Option<&fluent::FluentArgs<'_>>,
) -> Option<String> {
) -> Option<Cow<'static, str>> {
let message = bundle.get_message(id)?;
let pattern = match attr {
Some(attribute) => message.get_attribute(attribute)?.value(),
None => message.value()?,
};
let formatted = bundle.format_pattern(pattern, args, &mut vec![]);
Some(formatted.into_owned())
Some(formatted)
}

/// Retrieves the appropriate language file depending on user locale and calls [`format`]
pub fn get(
ctx: Context,
id: &str,
id: &'static str,
attr: Option<&str>,
args: Option<&fluent::FluentArgs<'_>>,
) -> String {
) -> Cow<'static, str> {
let translations = &ctx.data().translations;
ctx.locale()
// Try to get the language-specific translation
Expand All @@ -60,7 +62,7 @@ pub fn get(
// If this message ID is not present in any translation files whatsoever
.unwrap_or_else(|| {
tracing::warn!("unknown fluent message identifier `{}`", id);
id.to_string()
Cow::Borrowed(id)
})
}

Expand Down Expand Up @@ -97,7 +99,7 @@ pub fn read_ftl() -> Result<Translations, Error> {

/// Given a set of language files, fills in command strings and their localizations accordingly
pub fn apply_translations(
translations: &Translations,
translations: &'static Translations,
commands: &mut [poise::Command<Data, Error>],
) {
for command in &mut *commands {
Expand All @@ -108,21 +110,24 @@ pub fn apply_translations(
Some(x) => x,
None => continue, // no localization entry => skip localization
};
command
.name_localizations
.insert(locale.clone(), localized_command_name);
command.description_localizations.insert(

let locale = Cow::Borrowed(locale.as_str());
let name_localizations = command.name_localizations.to_mut();
let description_localizations = command.description_localizations.to_mut();

name_localizations.push((locale.clone(), localized_command_name));
description_localizations.push((
locale.clone(),
format(bundle, &command.name, Some("description"), None).unwrap(),
);
));

for parameter in &mut command.parameters {
// Insert localized parameter name and description
parameter.name_localizations.insert(
parameter.name_localizations.to_mut().push((
locale.clone(),
format(bundle, &command.name, Some(&parameter.name), None).unwrap(),
);
parameter.description_localizations.insert(
));
parameter.description_localizations.to_mut().push((
locale.clone(),
format(
bundle,
Expand All @@ -131,14 +136,14 @@ pub fn apply_translations(
None,
)
.unwrap(),
);
));

// If this is a choice parameter, insert its localized variants
for choice in &mut parameter.choices {
choice.localizations.insert(
for choice in parameter.choices.to_mut().iter_mut() {
choice.localizations.to_mut().push((
locale.clone(),
format(bundle, &choice.name, None, None).unwrap(),
);
));
}
}
}
Expand Down Expand Up @@ -170,7 +175,7 @@ pub fn apply_translations(
);

// If this is a choice parameter, set the choice names to en-US
for choice in &mut parameter.choices {
for choice in parameter.choices.to_mut().iter_mut() {
choice.name = format(bundle, &choice.name, None, None).unwrap();
}
}
Expand Down
Loading
Loading