uploads: centralise via ChatHubApi::save_upload, refactor handlers
Nightly Build / build (push) Successful in 6m41s
Nightly Build / build (push) Successful in 6m41s
Extract shared upload seam in skald-core, move Telegram and web handlers to use it. Simplify media attachment routing. Clean up unused deps and dead code.
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use core_api::message_meta::Attachment;
|
||||
use teloxide::net::Download;
|
||||
use teloxide::prelude::*;
|
||||
|
||||
@@ -10,9 +9,9 @@ use teloxide::prelude::*;
|
||||
/// # Extending
|
||||
/// Add a new variant here, then handle it in:
|
||||
/// 1. `handlers::classify_message` — detect the message type and build the variant
|
||||
/// 2. `TelegramAttachment::download_and_save` — fetch bytes and persist to disk,
|
||||
/// returning an [`Attachment`]
|
||||
/// (return `Ok(None)` if no file is involved)
|
||||
/// 2. `TelegramAttachment::download` — fetch the bytes (return `Ok(None)` if no
|
||||
/// file is involved); the caller persists them
|
||||
/// via the shared `ChatHubApi::save_upload` seam
|
||||
/// 3. `TelegramAttachment::system_info_message` — describe a file-less variant
|
||||
/// (Location) for the LLM
|
||||
pub(crate) enum TelegramAttachment {
|
||||
@@ -36,20 +35,16 @@ pub(crate) enum TelegramAttachment {
|
||||
}
|
||||
|
||||
impl TelegramAttachment {
|
||||
/// Downloads the attachment from Telegram, writes it to `base_dir/<chat_id>/<name>`,
|
||||
/// and returns the saved [`Attachment`] (shared with the web/mobile path so the
|
||||
/// copilot UI renders it identically). Returns `None` for attachment types that
|
||||
/// carry no binary content (e.g. Location).
|
||||
///
|
||||
/// The returned `path` is made relative to the process working directory (the
|
||||
/// project root) when possible, so it is both servable under `/data/…` and
|
||||
/// resolvable by the filesystem tools — matching web uploads.
|
||||
pub(crate) async fn download_and_save(
|
||||
/// Downloads the attachment's bytes from Telegram, returning
|
||||
/// `(file_name, mimetype, bytes)`. Persistence is **not** done here: the caller
|
||||
/// hands the bytes to the shared upload seam (`ChatHubApi::save_upload`), which
|
||||
/// saves them into the user's home under `uploads/{session}/…` and produces the
|
||||
/// [`Attachment`] — the same path every surface uses, so the agent can reach it.
|
||||
/// Returns `None` for attachment types that carry no binary content (e.g. Location).
|
||||
pub(crate) async fn download(
|
||||
&self,
|
||||
bot: &Bot,
|
||||
base_dir: &Path,
|
||||
chat_id: i64,
|
||||
) -> Result<Option<Attachment>> {
|
||||
bot: &Bot,
|
||||
) -> Result<Option<(String, Option<String>, Vec<u8>)>> {
|
||||
let (file_id, file_name, mimetype): (&str, String, Option<String>) = match self {
|
||||
Self::Document { file_id, file_name, mime_type, .. } =>
|
||||
(file_id, file_name.clone(), mime_type.clone()),
|
||||
@@ -58,28 +53,11 @@ impl TelegramAttachment {
|
||||
Self::Location { .. } => return Ok(None),
|
||||
};
|
||||
|
||||
let dir = base_dir.join(chat_id.to_string());
|
||||
tokio::fs::create_dir_all(&dir).await?;
|
||||
|
||||
let tg_file = bot.get_file(teloxide::types::FileId(file_id.to_string())).await?;
|
||||
let mut bytes = Vec::new();
|
||||
bot.download_file(&tg_file.path, &mut bytes).await?;
|
||||
|
||||
let path = dir.join(&file_name);
|
||||
tokio::fs::write(&path, &bytes).await?;
|
||||
|
||||
// Prefer a project-root-relative path so `/data/…` serving works.
|
||||
let rel = std::env::current_dir()
|
||||
.ok()
|
||||
.and_then(|cwd| path.strip_prefix(&cwd).ok().map(Path::to_path_buf))
|
||||
.unwrap_or_else(|| path.clone());
|
||||
|
||||
Ok(Some(Attachment {
|
||||
path: rel.to_string_lossy().to_string(),
|
||||
name: file_name,
|
||||
mimetype,
|
||||
filesize: Some(bytes.len() as u64),
|
||||
}))
|
||||
Ok(Some((file_name, mimetype, bytes)))
|
||||
}
|
||||
|
||||
/// Builds the `[TELEGRAM SYSTEM INFO]` message injected into the conversation history.
|
||||
|
||||
@@ -512,17 +512,32 @@ async fn handle_attachment(
|
||||
|
||||
bot.send_chat_action(chat_id, ChatAction::UploadDocument).await.ok();
|
||||
|
||||
let saved = match attachment.download_and_save(&bot, &shared.uploads_dir, chat_id.0).await {
|
||||
Ok(s) => s,
|
||||
let downloaded = match attachment.download(&bot).await {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
error!(error = %e, "telegram: failed to save attachment");
|
||||
bot.send_message(chat_id, "⚠️ Could not save the attachment.").await.ok();
|
||||
error!(error = %e, "telegram: failed to download attachment");
|
||||
bot.send_message(chat_id, "⚠️ Could not download the attachment.").await.ok();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match saved {
|
||||
Some(att) => {
|
||||
match downloaded {
|
||||
Some((file_name, mimetype, bytes)) => {
|
||||
// Persist through the shared upload seam so the file lands in the user's
|
||||
// home (`uploads/{session}/…`) with an agent-reachable path — identical
|
||||
// to a web upload.
|
||||
let att = match handle
|
||||
.chat_hub()
|
||||
.save_upload("telegram", &file_name, mimetype, &bytes)
|
||||
.await
|
||||
{
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
error!(error = %e, "telegram: failed to save attachment");
|
||||
bot.send_message(chat_id, "⚠️ Could not save the attachment.").await.ok();
|
||||
return;
|
||||
}
|
||||
};
|
||||
info!(chat_id = chat_id.0, path = %att.path, "telegram: attachment saved, forwarding to LLM");
|
||||
let caption = match &attachment {
|
||||
TelegramAttachment::Document { caption, .. } => caption.clone(),
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
/// `ApprovalApi`.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
@@ -106,7 +105,6 @@ pub(crate) struct TgShared {
|
||||
pub(crate) transcribe: Arc<dyn TranscribeProvider>,
|
||||
pub(crate) tts: Arc<dyn TtsProvider>,
|
||||
pub(crate) location: Arc<dyn LocationUpdater>,
|
||||
pub(crate) uploads_dir: PathBuf,
|
||||
|
||||
// ── Pairing / bindings (config-table-backed, cached in memory) ──
|
||||
pub(crate) bindings: RwLock<auth::TelegramConfig>,
|
||||
@@ -272,11 +270,6 @@ impl Plugin for TelegramPlugin {
|
||||
anyhow::bail!("telegram: token is empty — set it via the plugins API");
|
||||
}
|
||||
|
||||
let uploads_dir = std::env::current_dir()
|
||||
.unwrap_or_default()
|
||||
.join("uploads")
|
||||
.join("telegram");
|
||||
|
||||
// Load bindings from the config table (or default if absent).
|
||||
let telegram_config = auth::load_config(&*ctx.config).await
|
||||
.unwrap_or_default();
|
||||
@@ -293,7 +286,6 @@ impl Plugin for TelegramPlugin {
|
||||
transcribe: Arc::clone(&ctx.transcribe),
|
||||
tts: Arc::clone(&ctx.tts_provider),
|
||||
location: Arc::clone(&ctx.location),
|
||||
uploads_dir,
|
||||
bindings: RwLock::new(telegram_config),
|
||||
pending_approvals: Mutex::new(HashMap::new()),
|
||||
pending_questions: Mutex::new(HashMap::new()),
|
||||
|
||||
Reference in New Issue
Block a user