diff options
Diffstat (limited to 'src/commands')
| -rw-r--r-- | src/commands/music.rs | 24 | ||||
| -rw-r--r-- | src/commands/utilities.rs | 46 |
2 files changed, 57 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 @@ | |||
| 1 | use poise::serenity_prelude as serenity; | 1 | use poise::serenity_prelude as serenity; |
| 2 | use super::utilities::{FileType, extract_urls, file_type}; | ||
| 3 | |||
| 2 | use tracing::{error, info}; | 4 | use tracing::{error, info}; |
| 3 | 5 | ||
| 4 | use super::autocomplete::audio_file; | 6 | use super::autocomplete::audio_file; |
| @@ -459,6 +461,26 @@ async fn send_music_msg( | |||
| 459 | Ok(()) | 461 | Ok(()) |
| 460 | } | 462 | } |
| 461 | 463 | ||
| 464 | /// Right-click a message → play the first audio file, or first URL if no audio files. | ||
| 465 | #[poise::command(context_menu_command = "Odtwórz")] | ||
| 466 | pub async fn message_grajcuj(ctx: Context<'_>, message: serenity::Message) -> Result<()> { | ||
| 467 | // Files > links: prefer audio attachments first. | ||
| 468 | if let Some(att) = message | ||
| 469 | .attachments | ||
| 470 | .iter() | ||
| 471 | .find(|a| file_type(&a.filename) == FileType::Audio) | ||
| 472 | { | ||
| 473 | return play_impl(ctx, None, Some(att.clone()), false).await; | ||
| 474 | } | ||
| 475 | // Fallback: first URL in message content. | ||
| 476 | if let Some(url) = extract_urls(&message.content).into_iter().next() { | ||
| 477 | return play_impl(ctx, Some(url), None, false).await; | ||
| 478 | } | ||
| 479 | ctx.say(lang::L_MUSIC_NO_AUDIO_ATTACHMENTS).await?; | ||
| 480 | Ok(()) | ||
| 481 | } | ||
| 482 | |||
| 483 | |||
| 462 | pub fn commands() -> Vec<poise::Command<Data, Error>> { | 484 | pub fn commands() -> Vec<poise::Command<Data, Error>> { |
| 463 | vec![grajcuj(), stop(), join()] | 485 | vec![grajcuj(), stop(), join(), message_grajcuj()] |
| 464 | } | 486 | } |
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 { | |||
| 339 | .to_string() | 339 | .to_string() |
| 340 | } | 340 | } |
| 341 | 341 | ||
| 342 | /// Extract `https://` URLs containing a path (host + `/...`) from a message. | 342 | /// Extract `http://` and `https://` URLs from text. After consuming each URL, |
| 343 | /// Faithful port of Python's `Downloader.URL_PATTERN` regex used by the | 343 | /// trailing characters not in the RFC 3986 unreserved + sub-delims set are |
| 344 | /// message-context `download` command — only URLs with at least one `/` after | 344 | /// stripped (e.g. a trailing backtick that should have been URL-encoded). |
| 345 | /// the host are matched, and `http://` links are intentionally ignored. | 345 | pub(crate) fn extract_urls(content: &str) -> Vec<String> { |
| 346 | fn extract_urls(content: &str) -> Vec<String> { | 346 | fn is_url_char(b: u8) -> bool { |
| 347 | b.is_ascii_alphanumeric() | ||
| 348 | || matches!( | ||
| 349 | b, | ||
| 350 | b'-' | b'.' | b'_' | b'~' | b':' | b'/' | b'?' | b'#' | b'[' | b']' | b'@' | ||
| 351 | | b'!' | b'$' | b'&' | b'\'' | b'(' | b')' | b'*' | b'+' | b',' | b';' | ||
| 352 | | b'%' | b'=' | ||
| 353 | ) | ||
| 354 | } | ||
| 347 | let bytes = content.as_bytes(); | 355 | let bytes = content.as_bytes(); |
| 348 | let mut urls = Vec::new(); | 356 | let mut urls = Vec::new(); |
| 349 | let mut i = 0; | 357 | let mut i = 0; |
| 350 | while let Some(pos) = content[i..].find("https://") { | 358 | while i < bytes.len() { |
| 351 | let start = i + pos; | 359 | // Find next http:// or https:// |
| 352 | let after = start + "https://".len(); | 360 | let scheme = if bytes[i..].starts_with(b"https://") { |
| 361 | Some("https://") | ||
| 362 | } else if bytes[i..].starts_with(b"http://") { | ||
| 363 | Some("http://") | ||
| 364 | } else { | ||
| 365 | None | ||
| 366 | }; | ||
| 367 | let Some(s) = scheme else { | ||
| 368 | i += 1; | ||
| 369 | continue; | ||
| 370 | }; | ||
| 371 | let start = i; | ||
| 372 | let after = i + s.len(); | ||
| 353 | 373 | ||
| 354 | // Consume the host (until `/` or whitespace). | 374 | // Consume the host (until `/` or whitespace). |
| 355 | let mut j = after; | 375 | let mut j = after; |
| @@ -358,15 +378,17 @@ fn extract_urls(content: &str) -> Vec<String> { | |||
| 358 | } | 378 | } |
| 359 | 379 | ||
| 360 | if j < bytes.len() && bytes[j] == b'/' { | 380 | if j < bytes.len() && bytes[j] == b'/' { |
| 361 | // Consume the rest of the URL (until whitespace). | 381 | // Consume the rest of the URL (valid URL chars only). |
| 362 | let mut k = j; | 382 | let mut k = j; |
| 363 | while k < bytes.len() && !bytes[k].is_ascii_whitespace() { | 383 | while k < bytes.len() && is_url_char(bytes[k]) { |
| 364 | k += 1; | 384 | k += 1; |
| 365 | } | 385 | } |
| 366 | urls.push(content[start..k].to_string()); | 386 | if k > after { |
| 387 | urls.push(content[start..k].to_string()); | ||
| 388 | } | ||
| 367 | i = k; | 389 | i = k; |
| 368 | } else { | 390 | } else { |
| 369 | // No path component — not matched by the regex; advance past it. | 391 | // No path component — not matched; advance past it. |
| 370 | i = after; | 392 | i = after; |
| 371 | } | 393 | } |
| 372 | } | 394 | } |
