aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/commands/cron.rs21
-rw-r--r--src/commands/record.rs2
-rw-r--r--src/commands/trigger.rs12
-rw-r--r--src/commands/utilities.rs62
4 files changed, 58 insertions, 39 deletions
diff --git a/src/commands/cron.rs b/src/commands/cron.rs
index ec5b702..809d1a4 100644
--- a/src/commands/cron.rs
+++ b/src/commands/cron.rs
@@ -12,7 +12,6 @@ type Context<'a> = poise::Context<'a, Data, Error>;
12#[poise::command( 12#[poise::command(
13 slash_command, 13 slash_command,
14 guild_only, 14 guild_only,
15 required_permissions = "MANAGE_GUILD",
16 subcommands("dodaj_audio", "dodaj_wiadomość", "pokaż", "usuń", "pauza", "wznów",) 15 subcommands("dodaj_audio", "dodaj_wiadomość", "pokaż", "usuń", "pauza", "wznów",)
17)] 16)]
18pub async fn cron(_ctx: Context<'_>) -> Result<()> { 17pub async fn cron(_ctx: Context<'_>) -> Result<()> {
@@ -20,7 +19,7 @@ pub async fn cron(_ctx: Context<'_>) -> Result<()> {
20} 19}
21 20
22/// Schedule audio playback 21/// Schedule audio playback
23#[poise::command(slash_command, guild_only, required_permissions = "MANAGE_GUILD")] 22#[poise::command(slash_command, guild_only)]
24pub async fn dodaj_audio( 23pub async fn dodaj_audio(
25 ctx: Context<'_>, 24 ctx: Context<'_>,
26 #[description = "Cron expression (e.g. \"37 21 * * *\" = every day at 21:37)"] cron_expression: String, 25 #[description = "Cron expression (e.g. \"37 21 * * *\" = every day at 21:37)"] cron_expression: String,
@@ -75,7 +74,7 @@ pub async fn dodaj_audio(
75} 74}
76 75
77/// Schedule a periodic message 76/// Schedule a periodic message
78#[poise::command(slash_command, guild_only, required_permissions = "MANAGE_GUILD")] 77#[poise::command(slash_command, guild_only)]
79pub async fn dodaj_wiadomość( 78pub async fn dodaj_wiadomość(
80 ctx: Context<'_>, 79 ctx: Context<'_>,
81 #[description = "Cron expression (e.g. \"37 21 * * *\" = every day at 21:37)"] cron_expression: String, 80 #[description = "Cron expression (e.g. \"37 21 * * *\" = every day at 21:37)"] cron_expression: String,
@@ -109,7 +108,7 @@ pub async fn dodaj_wiadomość(
109} 108}
110 109
111/// List all cron jobs 110/// List all cron jobs
112#[poise::command(slash_command, guild_only, required_permissions = "MANAGE_GUILD")] 111#[poise::command(slash_command, guild_only)]
113pub async fn pokaż(ctx: Context<'_>) -> Result<()> { 112pub async fn pokaż(ctx: Context<'_>) -> Result<()> {
114 let guild_id = ctx.guild_id().ok_or_else(lang::err_not_in_guild)?; 113 let guild_id = ctx.guild_id().ok_or_else(lang::err_not_in_guild)?;
115 let jobs = database::list_cron_jobs(&ctx.data().db_pool, guild_id.get() as i64).await?; 114 let jobs = database::list_cron_jobs(&ctx.data().db_pool, guild_id.get() as i64).await?;
@@ -144,7 +143,7 @@ pub async fn pokaż(ctx: Context<'_>) -> Result<()> {
144} 143}
145 144
146/// Remove a cron job 145/// Remove a cron job
147#[poise::command(slash_command, guild_only, required_permissions = "MANAGE_GUILD")] 146#[poise::command(slash_command, guild_only)]
148pub async fn usuń( 147pub async fn usuń(
149 ctx: Context<'_>, 148 ctx: Context<'_>,
150 #[description = "ID pracy do usunięcia"] job_id: i64, 149 #[description = "ID pracy do usunięcia"] job_id: i64,
@@ -159,13 +158,15 @@ pub async fn usuń(
159} 158}
160 159
161/// Pause a cron job 160/// Pause a cron job
162#[poise::command(slash_command, guild_only, required_permissions = "MANAGE_GUILD")] 161#[poise::command(slash_command, guild_only)]
163pub async fn pauza( 162pub async fn pauza(
164 ctx: Context<'_>, 163 ctx: Context<'_>,
165 #[description = "ID pracy do zapauzowania"] job_id: i64, 164 #[description = "ID pracy do zapauzowania"] job_id: i64,
166) -> Result<()> { 165) -> Result<()> {
167 let guild_id = ctx.guild_id().ok_or_else(lang::err_not_in_guild)?; 166 let guild_id = ctx.guild_id().ok_or_else(lang::err_not_in_guild)?;
168 if database::set_cron_job_active(&ctx.data().db_pool, job_id, guild_id.get() as i64, false).await? { 167 if database::set_cron_job_active(&ctx.data().db_pool, job_id, guild_id.get() as i64, false)
168 .await?
169 {
169 ctx.say(lang::fmt_cron_paused(job_id)).await?; 170 ctx.say(lang::fmt_cron_paused(job_id)).await?;
170 } else { 171 } else {
171 ctx.say(lang::fmt_cron_not_found(job_id)).await?; 172 ctx.say(lang::fmt_cron_not_found(job_id)).await?;
@@ -174,13 +175,15 @@ pub async fn pauza(
174} 175}
175 176
176/// Resume a paused cron job 177/// Resume a paused cron job
177#[poise::command(slash_command, guild_only, required_permissions = "MANAGE_GUILD")] 178#[poise::command(slash_command, guild_only)]
178pub async fn wznów( 179pub async fn wznów(
179 ctx: Context<'_>, 180 ctx: Context<'_>,
180 #[description = "ID pracy cron do wznowienia"] job_id: i64, 181 #[description = "ID pracy cron do wznowienia"] job_id: i64,
181) -> Result<()> { 182) -> Result<()> {
182 let guild_id = ctx.guild_id().ok_or_else(lang::err_not_in_guild)?; 183 let guild_id = ctx.guild_id().ok_or_else(lang::err_not_in_guild)?;
183 if database::set_cron_job_active(&ctx.data().db_pool, job_id, guild_id.get() as i64, true).await? { 184 if database::set_cron_job_active(&ctx.data().db_pool, job_id, guild_id.get() as i64, true)
185 .await?
186 {
184 ctx.say(lang::fmt_cron_resumed(job_id)).await?; 187 ctx.say(lang::fmt_cron_resumed(job_id)).await?;
185 } else { 188 } else {
186 ctx.say(lang::fmt_cron_not_found(job_id)).await?; 189 ctx.say(lang::fmt_cron_not_found(job_id)).await?;
diff --git a/src/commands/record.rs b/src/commands/record.rs
index dfe736d..c5cecd3 100644
--- a/src/commands/record.rs
+++ b/src/commands/record.rs
@@ -13,7 +13,7 @@ type Context<'a> = poise::Context<'a, crate::Data, crate::error::Error>;
13/// connected to a voice channel, if `RECORDING_ENABLED=true`. This command is a 13/// connected to a voice channel, if `RECORDING_ENABLED=true`. This command is a
14/// read-only snapshot — the buffer keeps recording afterwards. Only callable by 14/// read-only snapshot — the buffer keeps recording afterwards. Only callable by
15/// someone in the same voice channel as the bot. 15/// someone in the same voice channel as the bot.
16#[poise::command(slash_command, guild_only, required_permissions = "MANAGE_GUILD")] 16#[poise::command(slash_command, guild_only)]
17pub async fn record(ctx: Context<'_>) -> Result<()> { 17pub async fn record(ctx: Context<'_>) -> Result<()> {
18 ctx.defer().await?; 18 ctx.defer().await?;
19 let data = ctx.data(); 19 let data = ctx.data();
diff --git a/src/commands/trigger.rs b/src/commands/trigger.rs
index 5d481e4..01fda18 100644
--- a/src/commands/trigger.rs
+++ b/src/commands/trigger.rs
@@ -20,7 +20,7 @@ pub async fn trigger(_ctx: Context<'_>) -> Result<()> {
20} 20}
21 21
22/// Create a new trigger group 22/// Create a new trigger group
23#[poise::command(slash_command, guild_only, required_permissions = "MANAGE_GUILD")] 23#[poise::command(slash_command, guild_only)]
24pub async fn create( 24pub async fn create(
25 ctx: Context<'_>, 25 ctx: Context<'_>,
26 #[description = "Logical group name"] 26 #[description = "Logical group name"]
@@ -41,7 +41,7 @@ pub async fn create(
41} 41}
42 42
43/// Add keywords and/or responses to a trigger group 43/// Add keywords and/or responses to a trigger group
44#[poise::command(slash_command, guild_only, required_permissions = "MANAGE_GUILD")] 44#[poise::command(slash_command, guild_only)]
45pub async fn add( 45pub async fn add(
46 ctx: Context<'_>, 46 ctx: Context<'_>,
47 #[description = "Existing group name"] 47 #[description = "Existing group name"]
@@ -83,7 +83,7 @@ pub async fn add(
83} 83}
84 84
85/// Remove keyword or response from a group 85/// Remove keyword or response from a group
86#[poise::command(slash_command, guild_only, required_permissions = "MANAGE_GUILD")] 86#[poise::command(slash_command, guild_only)]
87pub async fn remove( 87pub async fn remove(
88 ctx: Context<'_>, 88 ctx: Context<'_>,
89 #[description = "Group name"] 89 #[description = "Group name"]
@@ -115,7 +115,7 @@ pub async fn remove(
115} 115}
116 116
117/// Delete an entire trigger group 117/// Delete an entire trigger group
118#[poise::command(slash_command, guild_only, required_permissions = "MANAGE_GUILD")] 118#[poise::command(slash_command, guild_only)]
119pub async fn delete( 119pub async fn delete(
120 ctx: Context<'_>, 120 ctx: Context<'_>,
121 #[description = "Group name"] 121 #[description = "Group name"]
@@ -134,7 +134,7 @@ pub async fn delete(
134} 134}
135 135
136/// List keywords and responses for a group 136/// List keywords and responses for a group
137#[poise::command(slash_command, guild_only, required_permissions = "MANAGE_GUILD")] 137#[poise::command(slash_command, guild_only)]
138pub async fn list( 138pub async fn list(
139 ctx: Context<'_>, 139 ctx: Context<'_>,
140 #[description = "Group name"] 140 #[description = "Group name"]
@@ -185,7 +185,7 @@ pub async fn list(
185} 185}
186 186
187/// Display trigger command usage info 187/// Display trigger command usage info
188#[poise::command(slash_command, guild_only, required_permissions = "MANAGE_GUILD")] 188#[poise::command(slash_command, guild_only)]
189pub async fn help(ctx: Context<'_>) -> Result<()> { 189pub async fn help(ctx: Context<'_>) -> Result<()> {
190 let help = [ 190 let help = [
191 "**Trigger Commands**", 191 "**Trigger Commands**",
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