Skip to content

Commit

Permalink
Merge pull request #116 from CrawKatt/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
CrawKatt authored Nov 18, 2024
2 parents dce7227 + 531d9df commit 268d06a
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 152 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
[submodule "assets/rust-examples"]
path = assets/rust-examples
url = https://github.com/CrawKatt/rust-examples.git
[submodule "songbird"]
path = songbird
url = https://github.com/CrawKatt/songbird.git
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ symphonia = { version = "0.5.4", features = ["aac", "mp3", "isomp4", "alac"] }
poise = { git = "https://github.com/serenity-rs/poise.git", branch = "current" }
tokio = "1.28.1"
serenity = "0.12.0"
songbird = { version = "0.4.1", features = ["builtin-queue"] }
songbird = { path = "./songbird", features = ["builtin-queue"] }
serde = "1.0.164"
serde_json = "1.0.128"
reqwest = "0.11.22"
Expand Down
1 change: 1 addition & 0 deletions songbird
Submodule songbird added at 9e27f8
13 changes: 7 additions & 6 deletions src/commands/ai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ pub async fn ask(
let loading = ctx.say("Cargando...").await?;
let url = dotenvy::var("OPENAI_API_BASE")?;
let api_key = dotenvy::var("OPENAI_API_KEY")?;
let model = dotenvy::var("AI_MODEL")?;
let client = Client::new_with_endpoint(url, api_key);

let req = ChatCompletionRequest::new(
"meta/llama3-70b-instruct".to_string(),
model,
vec![
chat_completion::ChatCompletionMessage {
role: chat_completion::MessageRole::user,
Expand All @@ -35,27 +36,27 @@ pub async fn ask(
},
chat_completion::ChatCompletionMessage {
role: chat_completion::MessageRole::system,
content: chat_completion::Content::Text(String::from("Te llamas Plantita Ayudante. Nunca superes los 2000 carácteres en tus respuestas.")),
content: chat_completion::Content::Text(String::from("Te llamas Leafy. Nunca superes los 2000 carácteres en tus respuestas.")),
name: None,
}
],
).max_tokens(1024);

let result = client.chat_completion(req)?;
let message = result.choices[0].message.content.as_ref().into_result()?;

let action_row = vec![CreateActionRow::Buttons(vec![
CreateButton::new("close")
.style(ButtonStyle::Danger)
.label("Cerrar")
])
];

let reply = CreateReply::default()
.content(message)
.components(action_row);

loading.edit(ctx, reply).await?;

Ok(())
}
}
165 changes: 67 additions & 98 deletions src/commands/audio/play.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,10 @@
use crate::handlers::error::handler;
use crate::utils::debug::IntoUnwrapResult;
use serenity::all::{CreateEmbed, CreateEmbedAuthor, CreateMessage};
use songbird::input::YoutubeDl;
use crate::{HttpKey, location};
use crate::commands::audio::queue::AuxMetadataKey;
use crate::utils::{CommandResult, Context};
use poise::async_trait;
use songbird::{input, Event, EventContext, EventHandler, TrackEvent};
use std::fs;
use std::path::Path;
use std::process::Command;
use crate::utils::metadata::build_embed;

struct FileCleaner {
paths: Vec<String>,
}

#[async_trait]
impl EventHandler for FileCleaner {
async fn act(&self, _: &EventContext<'_>) -> Option<Event> {
for path in &self.paths {
if Path::new(path).exists() {
fs::remove_file(path).unwrap_or_else(|why| {
println!("No se pudo eliminar el archivo: {why}");
});
}
}
None
}
}
use crate::utils::debug::{IntoUnwrapResult, UnwrapLog};
use crate::handlers::error::handler;

#[poise::command(
prefix_command,
Expand All @@ -40,92 +20,81 @@ pub async fn play(
#[rest]
query: String
) -> CommandResult {
let do_search = !query.starts_with("http");
let guild = ctx.guild().into_result()?.clone();
let guild_id = guild.id;
super::try_join(ctx, guild).await?;

let author_name = ctx.author_member().await.into_result()?.distinct();
let author_face = ctx.author_member().await.into_result()?.face();
let manager = songbird::get(ctx.serenity_context()).await.into_result()?;
let author_name = ctx.author_member()
.await
.into_result()?
.distinct();

let author_face = ctx.author_member()
.await
.into_result()?
.face();

let http_client = {
let data = ctx.serenity_context().data.read().await;
data.get::<HttpKey>()
.cloned()
.unwrap_log(location!())?
};

let manager = songbird::get(ctx.serenity_context())
.await
.into_result()?;

let Some(handler_lock) = manager.get(guild_id) else {
ctx.say("No estás en un canal de voz").await?;
return Ok(())
};

let message = ctx.say("Descargando...").await?;
let output_path = format!("/tmp/{}.mp3", uuid::Uuid::new_v4());
let json_path = format!("{output_path}.info.json");
let limit_rate = "500K";
let message = ctx.say("Buscando...").await?;

// Intentar descargar el audio, reiniciando Tor si falla
try_download_with_retry(&ctx, &query, &output_path, limit_rate).await?;

// Si la descarga y reproducción son exitosas, continuar con la configuración de Songbird
// Necesario para bypassear el baneo de YouTube a Bots
// (No utilizar cookies de cuentas de Google personales)
let mut handler = handler_lock.lock().await;
let source = input::File::new(output_path.clone());
let track_handle = handler.enqueue_input(source.into()).await;
track_handle.add_event(
Event::Track(TrackEvent::End),
FileCleaner { paths: vec![output_path.clone(), json_path.clone()] },
)?;

build_embed(&ctx, &json_path, &author_name, &author_face).await?;
message.delete(ctx).await?;
let source = if do_search {
YoutubeDl::new_search(http_client, query)
} else {
YoutubeDl::new(http_client, query)
};

let mut src: songbird::input::Input = source.into();

// Obtener la metadata auxiliar de la pista, como el título y la miniatura
let aux_metadata = src.aux_metadata().await?;
let title = aux_metadata.title.clone().into_result()?;
let thumbnail = aux_metadata.thumbnail.clone().into_result()?;

// Insertar la pista en la cola de reproducción
let track = handler.enqueue_input(src).await;

// Insertar la metadata en el TypeMap, para poder acceder
// a la metadata de la cola de reproducción
let mut map = track.typemap().write().await;
map.entry::<AuxMetadataKey>().or_insert(aux_metadata);

let song_name = if handler.queue().is_empty() { format!("Reproduciendo {title}") } else { format!("{title} Añadido a la cola") };

message.delete(ctx).await?;
let desc = format!("**Solicitado por:** {author_name}");
let embed = CreateEmbed::new()
.title(song_name)
.author(CreateEmbedAuthor::new(author_name)
.icon_url(author_face))
.description(desc)
.thumbnail(thumbnail)
.color(0x00ff_0000);

let builder = CreateMessage::new().embed(embed);
ctx.channel_id().send_message(ctx.http(), builder).await?;

// Liberar el bloqueo del manejador y del map para evitar fugas de memoria
drop(handler);
drop(map);

Ok(())
}

/// Ejecuta el comando yt-dlp para descargar audio y devuelve su estado.
fn download_audio(query: &str, output_path: &str, limit_rate: &str) -> std::io::Result<std::process::ExitStatus> {
Command::new("yt-dlp")
.arg("-x")
.arg("--audio-format")
.arg("mp3")
.arg("--add-metadata")
.arg("--write-info-json")
.arg("--limit-rate")
.arg(limit_rate)
.arg("-o")
.arg(output_path)
.arg("--proxy")
.arg("socks5://127.0.0.1:9050")
.arg(query)
.status()
}

/// Reinicia el servicio de Tor para permitir otra descarga.
async fn restart_tor_service(ctx: &Context<'_>) -> CommandResult {
ctx.say("Error en la descarga. Reintentando...").await?;
let restart_status = Command::new("sudo")
.arg("service")
.arg("tor")
.arg("restart")
.status();

if let Ok(status) = restart_status {
if status.success() {
ctx.say("Reintentando la descarga...").await?;
return Ok(());
}
}
ctx.say("Error al reintentar la descarga").await?;
Err("Failed to restart Tor service".into())
}

/// Intenta descargar el audio y reinicia Tor en caso de fallo.
async fn try_download_with_retry(ctx: &Context<'_>, query: &str, output_path: &str, limit_rate: &str) -> CommandResult {
let status = download_audio(query, output_path, limit_rate)?;

if !status.success() {
restart_tor_service(ctx).await?;
let retry_status = download_audio(query, output_path, limit_rate)?;
if !retry_status.success() {
ctx.say("Error al descargar el audio 403").await?;
return Err("Download failed after Tor restart".into());
}
}
Ok(())
}
3 changes: 2 additions & 1 deletion src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ pub mod moderation;
pub mod audio;
pub mod info;
pub mod lessons;
pub mod ai;
pub mod ai;
pub mod translate;
73 changes: 73 additions & 0 deletions src/commands/translate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use crate::utils::{CommandResult, Context};
use openai_api_rs::v1::api::Client;
use openai_api_rs::v1::chat_completion;
use openai_api_rs::v1::chat_completion::ChatCompletionRequest;
use poise::CreateReply;
use serenity::all::{ButtonStyle, CreateButton};
use serenity::builder::CreateActionRow;
use crate::utils::debug::IntoUnwrapResult;

const SYSTEM_PROMPT: &str =
"Eres un traductor multilingüe que convierte texto de un idioma a otro. Responde únicamente\
con la traducción exacta del texto proporcionado, utilizando el alfabeto nativo del idioma de salida.\
No uses transliteraciones (como romanji en japonés) ni caracteres que no sean propios del idioma de salida.\
La respuesta debe ser limpia, sin paréntesis, notas, ni comentarios adicionales. Ejemplo:\
Texto de entrada: 'Hola, ¿cómo estás?'\
Traducción esperada (japonés): 'こんにちは、お元気ですか?'\
Traducción incorrecta: 'Konnichiwa, ogenki desu ka?'";

#[poise::command(
prefix_command,
slash_command,
guild_only,
category = "Info",
aliases("tr"),
guild_cooldown = 15,
)]
pub async fn translate(
ctx: Context<'_>,
lang: String,
#[description = "Texto a enviar al modelo de IA"]
#[rest]
prompt: String
) -> CommandResult {
let loading = ctx.say("Cargando...").await?;
let url = dotenvy::var("OPENAI_API_BASE")?;
let api_key = dotenvy::var("OPENAI_API_KEY")?;
let model = dotenvy::var("AI_MODEL")?;
let client = Client::new_with_endpoint(url, api_key);

let req = ChatCompletionRequest::new(
model,
vec![
chat_completion::ChatCompletionMessage {
role: chat_completion::MessageRole::system,
content: chat_completion::Content::Text(format!("{SYSTEM_PROMPT}. Idioma de salida: {lang}")),
name: None,
},
chat_completion::ChatCompletionMessage {
role: chat_completion::MessageRole::user,
content: chat_completion::Content::Text(prompt),
name: None,
}
],
).max_tokens(1024);

let result = client.chat_completion(req)?;
let message = result.choices[0].message.content.as_ref().into_result()?;

let action_row = vec![CreateActionRow::Buttons(vec![
CreateButton::new("close")
.style(ButtonStyle::Danger)
.label("Cerrar")
])
];

let reply = CreateReply::default()
.content(message)
.components(action_row);

loading.edit(ctx, reply).await?;

Ok(())
}
44 changes: 0 additions & 44 deletions src/utils/metadata.rs

This file was deleted.

Loading

0 comments on commit 268d06a

Please sign in to comment.