Skip to content

Commit 80f3374

Browse files
authored
Merge pull request #93 from Marenz/telegram-polls
Add native poll support to telegram adapter
2 parents e20ff9f + 97e410a commit 80f3374

1 file changed

Lines changed: 72 additions & 4 deletions

File tree

src/messaging/telegram.rs

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ use teloxide::Bot;
1010
use teloxide::payloads::setters::*;
1111
use teloxide::requests::{Request, Requester};
1212
use teloxide::types::{
13-
ChatAction, ChatId, FileId, InputFile, MediaKind, MessageId, MessageKind, ReactionType,
14-
ReplyParameters, UpdateKind, UserId,
13+
ChatAction, ChatId, FileId, InputFile, InputPollOption, MediaKind, MessageId, MessageKind,
14+
ReactionType, ReplyParameters, UpdateKind, UserId,
1515
};
1616

1717
use std::collections::{HashMap, VecDeque};
@@ -218,6 +218,11 @@ impl Messaging for TelegramAdapter {
218218
} else if let Some(filter) = &permissions.chat_filter {
219219
// Chat filter: if configured, only allow listed group/channel chats
220220
if !filter.contains(&chat_id) {
221+
tracing::debug!(
222+
chat_id,
223+
?filter,
224+
"telegram message rejected by chat filter"
225+
);
221226
continue;
222227
}
223228
}
@@ -289,7 +294,7 @@ impl Messaging for TelegramAdapter {
289294
.context("failed to send telegram message")?;
290295
}
291296
}
292-
OutboundResponse::RichMessage { text, .. } => {
297+
OutboundResponse::RichMessage { text, poll, .. } => {
293298
self.stop_typing(&message.conversation_id).await;
294299

295300
for chunk in split_message(&text, MAX_MESSAGE_LENGTH) {
@@ -299,6 +304,10 @@ impl Messaging for TelegramAdapter {
299304
.await
300305
.context("failed to send telegram message")?;
301306
}
307+
308+
if let Some(poll_data) = poll {
309+
send_poll(&self.bot, chat_id, &poll_data).await?;
310+
}
302311
}
303312
OutboundResponse::ThreadReply {
304313
thread_name: _,
@@ -492,14 +501,18 @@ impl Messaging for TelegramAdapter {
492501
.await
493502
.context("failed to broadcast telegram message")?;
494503
}
495-
} else if let OutboundResponse::RichMessage { text, .. } = response {
504+
} else if let OutboundResponse::RichMessage { text, poll, .. } = response {
496505
for chunk in split_message(&text, MAX_MESSAGE_LENGTH) {
497506
self.bot
498507
.send_message(chat_id, &chunk)
499508
.send()
500509
.await
501510
.context("failed to broadcast telegram message")?;
502511
}
512+
513+
if let Some(poll_data) = poll {
514+
send_poll(&self.bot, chat_id, &poll_data).await?;
515+
}
503516
}
504517

505518
Ok(())
@@ -811,6 +824,61 @@ fn build_display_name(user: &teloxide::types::User) -> String {
811824
}
812825
}
813826

827+
/// Send a native Telegram poll.
828+
///
829+
/// Telegram limits: max 12 answer options, question max 300 chars, each option
830+
/// max 100 chars. `open_period` only supports 5–600 seconds so we only set it
831+
/// when `duration_hours` converts to ≤600s; otherwise the poll stays open
832+
/// indefinitely (until manually stopped via the Telegram client).
833+
async fn send_poll(bot: &Bot, chat_id: ChatId, poll: &crate::Poll) -> anyhow::Result<()> {
834+
let question = if poll.question.len() > 300 {
835+
format!("{}…", &poll.question[..poll.question.floor_char_boundary(299)])
836+
} else {
837+
poll.question.clone()
838+
};
839+
840+
let options: Vec<InputPollOption> = poll
841+
.answers
842+
.iter()
843+
.take(12)
844+
.map(|answer| {
845+
let text = if answer.len() > 100 {
846+
format!("{}…", &answer[..answer.floor_char_boundary(99)])
847+
} else {
848+
answer.clone()
849+
};
850+
InputPollOption::new(text)
851+
})
852+
.collect();
853+
854+
if options.len() < 2 {
855+
anyhow::bail!("telegram polls require at least 2 answer options");
856+
}
857+
858+
let mut request = bot
859+
.send_poll(chat_id, question, options)
860+
.is_anonymous(false);
861+
862+
// Telegram's open_period only supports 5–600 seconds. Apply it when the
863+
// requested duration fits; otherwise leave unset so the poll stays open
864+
// indefinitely.
865+
let duration_secs = poll.duration_hours.saturating_mul(3600);
866+
if (5..=600).contains(&duration_secs) {
867+
request = request.open_period(duration_secs as u16);
868+
}
869+
870+
if poll.allow_multiselect {
871+
request = request.allows_multiple_answers(true);
872+
}
873+
874+
request
875+
.send()
876+
.await
877+
.context("failed to send telegram poll")?;
878+
879+
Ok(())
880+
}
881+
814882
/// Split a message into chunks that fit within Telegram's character limit.
815883
/// Tries to split at newlines, then spaces, then hard-cuts.
816884
fn split_message(text: &str, max_len: usize) -> Vec<String> {

0 commit comments

Comments
 (0)