From f512d9d22dc3b57bd53834c393304081e9fd4faa Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 9 Sep 2026 13:25:45 +0200 Subject: message-contextual /grajcuj Signed-off-by: salmonik --- src/commands/music.rs | 24 +++++++++++++++++++++++- src/commands/utilities.rs | 46 ++++++++++++++++++++++++++++++++++------------ src/lang.rs | 1 + 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/commands/music.rs b/src/commands/music.rs index 019a015..e528bf5 100644 --- a/src/commands/music.rs +++ b/src/commands/music.rs @@ -1,4 +1,6 @@ use poise::serenity_prelude as serenity; +use super::utilities::{FileType, extract_urls, file_type}; + use tracing::{error, info}; use super::autocomplete::audio_file; @@ -459,6 +461,26 @@ async fn send_music_msg( Ok(()) } +/// Right-click a message → play the first audio file, or first URL if no audio files. +#[poise::command(context_menu_command = "Odtwórz")] +pub async fn message_grajcuj(ctx: Context<'_>, message: serenity::Message) -> Result<()> { + // Files > links: prefer audio attachments first. + if let Some(att) = message + .attachments + .iter() + .find(|a| file_type(&a.filename) == FileType::Audio) + { + return play_impl(ctx, None, Some(att.clone()), false).await; + } + // Fallback: first URL in message content. + if let Some(url) = extract_urls(&message.content).into_iter().next() { + return play_impl(ctx, Some(url), None, false).await; + } + ctx.say(lang::L_MUSIC_NO_AUDIO_ATTACHMENTS).await?; + Ok(()) +} + + pub fn commands() -> Vec> { - vec![grajcuj(), stop(), join()] + vec![grajcuj(), stop(), join(), message_grajcuj()] } diff --git a/src/commands/utilities.rs b/src/commands/utilities.rs index 337979e..87a5089 100644 --- a/src/commands/utilities.rs +++ b/src/commands/utilities.rs @@ -339,17 +339,37 @@ fn sanitize_filename(name: &str) -> String { .to_string() } -/// Extract `https://` URLs containing a path (host + `/...`) from a message. -/// Faithful port of Python's `Downloader.URL_PATTERN` regex used by the -/// message-context `download` command — only URLs with at least one `/` after -/// the host are matched, and `http://` links are intentionally ignored. -fn extract_urls(content: &str) -> Vec { +/// Extract `http://` and `https://` URLs from text. After consuming each URL, +/// trailing characters not in the RFC 3986 unreserved + sub-delims set are +/// stripped (e.g. a trailing backtick that should have been URL-encoded). +pub(crate) fn extract_urls(content: &str) -> Vec { + fn is_url_char(b: u8) -> bool { + b.is_ascii_alphanumeric() + || matches!( + b, + b'-' | b'.' | b'_' | b'~' | b':' | b'/' | b'?' | b'#' | b'[' | b']' | b'@' + | b'!' | b'$' | b'&' | b'\'' | b'(' | b')' | b'*' | b'+' | b',' | b';' + | b'%' | b'=' + ) + } let bytes = content.as_bytes(); let mut urls = Vec::new(); let mut i = 0; - while let Some(pos) = content[i..].find("https://") { - let start = i + pos; - let after = start + "https://".len(); + while i < bytes.len() { + // Find next http:// or https:// + let scheme = if bytes[i..].starts_with(b"https://") { + Some("https://") + } else if bytes[i..].starts_with(b"http://") { + Some("http://") + } else { + None + }; + let Some(s) = scheme else { + i += 1; + continue; + }; + let start = i; + let after = i + s.len(); // Consume the host (until `/` or whitespace). let mut j = after; @@ -358,15 +378,17 @@ fn extract_urls(content: &str) -> Vec { } if j < bytes.len() && bytes[j] == b'/' { - // Consume the rest of the URL (until whitespace). + // Consume the rest of the URL (valid URL chars only). let mut k = j; - while k < bytes.len() && !bytes[k].is_ascii_whitespace() { + while k < bytes.len() && is_url_char(bytes[k]) { k += 1; } - urls.push(content[start..k].to_string()); + if k > after { + urls.push(content[start..k].to_string()); + } i = k; } else { - // No path component — not matched by the regex; advance past it. + // No path component — not matched; advance past it. i = after; } } diff --git a/src/lang.rs b/src/lang.rs index de31988..238d2df 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -94,6 +94,7 @@ pub const L_MUSIC_NO_URL_IN_RESULT: &str = "Wynik nie ma URL"; pub const L_MUSIC_CANT_COMPRESS_LIVE: &str = "Nie można skompresować transmisji na żywo, odtwarzam normalnie"; pub const L_MUSIC_STOPPED: &str = "wyszed"; +pub const L_MUSIC_NO_AUDIO_ATTACHMENTS: &str = "Brak plików audio w wiadomości"; pub const L_VOICE_USER_NOT_IN_VC: &str = "You are not in a voice channel"; pub fn fmt_music_search_failed(e: &dyn std::fmt::Display) -> String { -- cgit v1.2.3