aboutsummaryrefslogtreecommitdiffstats
path: root/src/commands/utilities.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-06 21:34:53 +0100
committersalmonik <salmonik@tuta.io>2026-11-06 21:34:53 +0100
commitf471d4caefff51d49b69551412f4859d99f674a2 (patch)
tree1b48ef33957c39522eaaa599324e1d01ac85178f /src/commands/utilities.rs
parenttests: skip the piper smoke test when models are absent (diff)
downloadsenator-sperma-main.tar.gz
senator-sperma-main.zip
revert required permissionsmain
Signed-off-by: salmonik <salmonik@tuta.io>
Diffstat (limited to 'src/commands/utilities.rs')
-rw-r--r--src/commands/utilities.rs62
1 files changed, 39 insertions, 23 deletions
diff --git a/src/commands/utilities.rs b/src/commands/utilities.rs
index f43b30e..f585d83 100644
--- a/src/commands/utilities.rs
+++ b/src/commands/utilities.rs
@@ -4,11 +4,11 @@ use poise::serenity_prelude as serenity;
4use tracing::warn; 4use tracing::warn;
5 5
6use super::autocomplete::any_file; 6use super::autocomplete::any_file;
7use crate::music::sanitize_yt_url;
8use crate::Data; 7use crate::Data;
9use crate::copyparty::{html_decode, percent_decode, redact_url}; 8use crate::copyparty::{html_decode, percent_decode, redact_url};
10use crate::error::{Error, Result}; 9use crate::error::{Error, Result};
11use crate::lang; 10use crate::lang;
11use crate::music::sanitize_yt_url;
12 12
13type Context<'a> = poise::Context<'a, Data, Error>; 13type Context<'a> = poise::Context<'a, Data, Error>;
14 14
@@ -90,19 +90,12 @@ fn safe_basename(name: &str) -> String {
90/// Create a unique per-operation directory and return 90/// Create a unique per-operation directory and return
91/// `(directory, input_path, output_path)`. Concurrent commands can never 91/// `(directory, input_path, output_path)`. Concurrent commands can never
92/// overwrite or delete each other's files. 92/// overwrite or delete each other's files.
93async fn temp_op( 93async fn temp_op(original: &str, output_name: &str) -> std::io::Result<(String, String, String)> {
94 original: &str,
95 output_name: &str,
96) -> std::io::Result<(String, String, String)> {
97 let safe = safe_basename(original); 94 let safe = safe_basename(original);
98 let ext = ext_of(&safe) 95 let ext = ext_of(&safe)
99 .map(|ext| format!(".{ext}")) 96 .map(|ext| format!(".{ext}"))
100 .unwrap_or_default(); 97 .unwrap_or_default();
101 let dir = format!( 98 let dir = format!("{}/op_{}", crate::config::temp_path(), uuid::Uuid::new_v4());
102 "{}/op_{}",
103 crate::config::temp_path(),
104 uuid::Uuid::new_v4()
105 );
106 tokio::fs::create_dir_all(&dir).await?; 99 tokio::fs::create_dir_all(&dir).await?;
107 let input_path = format!("{dir}/input{ext}"); 100 let input_path = format!("{dir}/input{ext}");
108 let output_path = format!("{dir}/{}", safe_basename(output_name)); 101 let output_path = format!("{dir}/{}", safe_basename(output_name));
@@ -303,7 +296,10 @@ async fn compress_attachment_bytes(
303/// Compress audio bytes for playback using Vorbis (symphonia-decodable). 296/// Compress audio bytes for playback using Vorbis (symphonia-decodable).
304/// Uses Vorbis instead of Opus because symphonia 0.6 lacks an Opus decoder. 297/// Uses Vorbis instead of Opus because symphonia 0.6 lacks an Opus decoder.
305/// Returns the compressed .ogg bytes on success. 298/// Returns the compressed .ogg bytes on success.
306pub(crate) async fn compress_audio_bytes(input_bytes: &[u8], original_name: &str) -> Option<Vec<u8>> { 299pub(crate) async fn compress_audio_bytes(
300 input_bytes: &[u8],
301 original_name: &str,
302) -> Option<Vec<u8>> {
307 let safe_name = safe_basename(original_name); 303 let safe_name = safe_basename(original_name);
308 let stem = stem_of(&safe_name).to_string(); 304 let stem = stem_of(&safe_name).to_string();
309 let out_filename = format!("compressed_{stem}.ogg"); 305 let out_filename = format!("compressed_{stem}.ogg");
@@ -378,9 +374,28 @@ pub(crate) fn extract_urls(content: &str) -> Vec<String> {
378 b.is_ascii_alphanumeric() 374 b.is_ascii_alphanumeric()
379 || matches!( 375 || matches!(
380 b, 376 b,
381 b'-' | b'.' | b'_' | b'~' | b':' | b'/' | b'?' | b'#' | b'[' | b']' | b'@' 377 b'-' | b'.'
382 | b'!' | b'$' | b'&' | b'\'' | b'(' | b')' | b'*' | b'+' | b',' | b';' 378 | b'_'
383 | b'%' | b'=' 379 | b'~'
380 | b':'
381 | b'/'
382 | b'?'
383 | b'#'
384 | b'['
385 | b']'
386 | b'@'
387 | b'!'
388 | b'$'
389 | b'&'
390 | b'\''
391 | b'('
392 | b')'
393 | b'*'
394 | b'+'
395 | b','
396 | b';'
397 | b'%'
398 | b'='
384 ) 399 )
385 } 400 }
386 let bytes = content.as_bytes(); 401 let bytes = content.as_bytes();
@@ -445,9 +460,10 @@ async fn find_downloaded_file(dir: &str) -> Option<(String, String)> {
445 Err(_) => continue, 460 Err(_) => continue,
446 }; 461 };
447 if let Some((t, _)) = &newest 462 if let Some((t, _)) = &newest
448 && *t >= mtime { 463 && *t >= mtime
449 continue; 464 {
450 } 465 continue;
466 }
451 newest = Some((mtime, entry.path().to_string_lossy().into_owned())); 467 newest = Some((mtime, entry.path().to_string_lossy().into_owned()));
452 } 468 }
453 newest.map(|(_, p)| { 469 newest.map(|(_, p)| {
@@ -540,8 +556,10 @@ async fn run_download(
540 Ok(result) => result.map_err(|e| Error::Custom(lang::fmt_ytdlp_err(&e)))?, 556 Ok(result) => result.map_err(|e| Error::Custom(lang::fmt_ytdlp_err(&e)))?,
541 Err(_) => { 557 Err(_) => {
542 let _ = tokio::fs::remove_dir_all(&dl_dir).await; 558 let _ = tokio::fs::remove_dir_all(&dl_dir).await;
543 ctx.say(lang::fmt_download_failed("download timed out after 10 minutes")) 559 ctx.say(lang::fmt_download_failed(
544 .await?; 560 "download timed out after 10 minutes",
561 ))
562 .await?;
545 return Ok(()); 563 return Ok(());
546 } 564 }
547 }; 565 };
@@ -1043,7 +1061,7 @@ pub async fn message_download(ctx: Context<'_>, message: serenity::Message) -> R
1043} 1061}
1044 1062
1045/// Echo text to channel 1063/// Echo text to channel
1046#[poise::command(slash_command, required_permissions = "MANAGE_MESSAGES")] 1064#[poise::command(slash_command)]
1047pub async fn say(ctx: Context<'_>, #[description = "Message to send"] text: String) -> Result<()> { 1065pub async fn say(ctx: Context<'_>, #[description = "Message to send"] text: String) -> Result<()> {
1048 // Acknowledge the interaction with an invisible ephemeral reply, delete it, 1066 // Acknowledge the interaction with an invisible ephemeral reply, delete it,
1049 // then post the actual text as a normal channel message (not a reply to the 1067 // then post the actual text as a normal channel message (not a reply to the
@@ -1059,9 +1077,7 @@ pub async fn say(ctx: Context<'_>, #[description = "Message to send"] text: Stri
1059 warn!("say: failed to delete ephemeral acknowledgement: {e}"); 1077 warn!("say: failed to delete ephemeral acknowledgement: {e}");
1060 } 1078 }
1061 1079
1062 ctx.channel_id() 1080 ctx.channel_id().say(ctx.serenity_context(), &text).await?;
1063 .say(ctx.serenity_context(), &text)
1064 .await?;
1065 Ok(()) 1081 Ok(())
1066} 1082}
1067 1083