aboutsummaryrefslogtreecommitdiffstats
path: root/src/commands
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-09-09 13:25:45 +0200
committersalmonik <salmonik@tuta.io>2026-09-09 13:25:45 +0200
commitf512d9d22dc3b57bd53834c393304081e9fd4faa (patch)
tree9e959541f4a01c650b31d8b0fb9d6e6118230853 /src/commands
parentassistant replies to the message (diff)
downloadsenator-sperma-f512d9d22dc3b57bd53834c393304081e9fd4faa.tar.gz
senator-sperma-f512d9d22dc3b57bd53834c393304081e9fd4faa.zip
message-contextual /grajcuj
Signed-off-by: salmonik <salmonik@tuta.io>
Diffstat (limited to '')
-rw-r--r--src/commands/music.rs24
-rw-r--r--src/commands/utilities.rs46
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 @@
1use poise::serenity_prelude as serenity; 1use poise::serenity_prelude as serenity;
2use super::utilities::{FileType, extract_urls, file_type};
3
2use tracing::{error, info}; 4use tracing::{error, info};
3 5
4use super::autocomplete::audio_file; 6use 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")]
466pub 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
462pub fn commands() -> Vec<poise::Command<Data, Error>> { 484pub 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. 345pub(crate) fn extract_urls(content: &str) -> Vec<String> {
346fn 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 }