PollCard is a builder pattern for rendering a styled poll image as a Vec<u8> PNG, ready to send as a file attachment.
Setup
Add toCargo.toml:
Copy
fluxer-poll = "0.3.1"
Limits
| Limit | Maximum |
|---|---|
options count | 7 items |
render_png() will return an error if the options count is empty or exceeds 7 items.Creating
PollCard::new
Copy
pub fn new(title: impl Into<String>) -> Self
title displayed in the header.
Methods
option
Copy
pub fn option(self, label: impl Into<String>, votes: u32) -> Self
label and votes count. The percentages and progress bars are calculated automatically based on the total sum of votes across all options.
You can call this method up to 7 times. Exceeding this limit will cause
render_png() to fail.header_label
Copy
pub fn header_label(self, label: impl Into<String>) -> Self
"POLL".
votes_label
Copy
pub fn votes_label(self, label: impl Into<String>) -> Self
"votes".
render_png
Copy
pub fn render_png(&self) -> Result<Vec<u8>>
Vec<u8> that can be attached as a file.
Example
Simple usage
Simple usage
Copy
use fluxer_poll::PollCard;
use fluxer_builders::{MessagePayload, build_multipart_form, file::FileAttachment};
let png = PollCard::new("Best language?")
.option("Rust", 42)
.option("TypeScript", 17)
.option("Python", 8)
.render_png()?;
let file = FileAttachment::new("poll.png", png);
let payload = MessagePayload::new()
.content("**Best language?**")
.build();
let form = build_multipart_form(&payload, &[file]);
rest.post_multipart::<serde_json::Value>(
&Routes::channel_messages(&channel_id),
form,
).await?;
Full Bot Example
A complete minimal bot implementing a!poll command. The bot dynamically updates the image in place using edit_files as users react to vote.
Full Bot Example
Full Bot Example
Cargo.toml
Copy
[dependencies]
fluxer-core = { version = "0.3.1", default-features = false }
fluxer-builders = "0.3.1"
fluxer-rest = "0.3.1"
fluxer-types = "0.3.1"
fluxer-poll = "0.3.1"
serde_json = "1"
base64 = "0.22"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing = "0.1"
rustls = { version = "0.23", features = ["ring"] }
Copy
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
use fluxer_builders::{build_multipart_form, file::FileAttachment, MessagePayload};
use fluxer_core::client::{typed_events::DispatchEvent, Client, ClientOptions};
use fluxer_poll::PollCard;
use fluxer_rest::Rest;
use fluxer_types::Routes;
use serde_json::Value;
const TOKEN: &str = "1478692632696393917.YeM3H8n7uCc91u5IN_D0iNjDwgRyC18AF-21exVY8uI";
const EMOJIS: [&str; 7] = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣"];
type PollStore = Arc<Mutex<HashMap<String, (String, String, Vec<String>)>>>;
#[tokio::main]
async fn main() {
rustls::crypto::ring::default_provider()
.install_default()
.expect("Failed to install rustls crypto provider");
tracing_subscriber::fmt().with_env_filter("info").init();
let mut client = Client::new(ClientOptions {
intents: 0,
wait_for_guilds: true,
..Default::default()
});
let rest: Rest = client.rest.clone();
let polls: PollStore = Arc::new(Mutex::new(HashMap::new()));
client.on_typed(move |event| {
let rest = rest.clone();
let polls = polls.clone();
Box::pin(async move {
match event {
DispatchEvent::Ready => tracing::info!("Bot is ready"),
DispatchEvent::MessageCreate { message, .. } => {
let text = message.content.trim();
let Some(args) = text.strip_prefix("!poll") else { return };
let parts: Vec<&str> = args.split('|').map(str::trim).collect();
if parts.len() < 3 { return; }
let title = parts[0].to_string();
let labels: Vec<String> = parts[1..].iter().map(|s| s.to_string()).collect();
let mut card = PollCard::new(&title);
for l in &labels { card = card.option(l, 0); }
let Ok(png) = card.render_png() else { return };
let payload = MessagePayload::new()
.content(format!("**{title}**"))
.build();
let file = FileAttachment::new("poll.png", png);
let form = build_multipart_form(&payload, &[file]);
if let Ok(val) = rest.post_multipart::<Value>(
&Routes::channel_messages(&message.channel_id),
form,
).await {
let id = val["id"].as_str().unwrap_or("").to_string();
if let Some(msg) = fluxer_core::structures::message::Message::from_value(&val) {
for emoji in EMOJIS.iter().take(labels.len()) {
let _ = msg.add_reaction(&rest, emoji).await;
}
}
polls.lock().await.insert(id, (message.channel_id, title, labels));
}
}
DispatchEvent::MessageReactionAdd { reaction } => {
let (channel_id, title, labels) = {
let map = polls.lock().await;
let Some(e) = map.get(&reaction.message_id) else { return };
e.clone()
};
let valid = EMOJIS.iter().take(labels.len()).any(|e| *e == reaction.emoji_name.as_str());
if !valid {
let _ = rest.delete_route(&format!(
"{}/{}",
Routes::channel_message_reaction(&channel_id, &reaction.message_id, &reaction.emoji_name),
reaction.user_id,
)).await;
return;
}
let mut card = PollCard::new(&title);
for (i, label) in labels.iter().enumerate() {
let count = rest.get::<Vec<Value>>(&format!(
"{}?limit=100",
Routes::channel_message_reaction(&channel_id, &reaction.message_id, EMOJIS[i]),
)).await.map(|u| u.len().saturating_sub(1) as u32).unwrap_or(0);
card = card.option(label, count);
}
match card.render_png() {
Ok(png) => {
if let Ok(val) = rest.get::<Value>(&Routes::channel_message(&channel_id, &reaction.message_id)).await {
if let Some(msg) = fluxer_core::structures::message::Message::from_value(&val) {
let payload = MessagePayload::new().content(format!("**{title}**")).build();
let file = FileAttachment::new("poll.png", png);
let _ = msg.edit_files(&rest, &payload, &[file]).await;
}
}
}
Err(e) => tracing::error!("Render error: {e}"),
}
}
_ => {}
}
})
});
if let Err(e) = client.login(TOKEN).await {
tracing::error!("login: {e:?}");
}
}
