use std::sync::Arc;
use turnframe_core::ids::{AttachmentId, TurnId};
use turnframe_core::locale::{Locale, LocalizedText};
use turnframe_core::turn::{AttachmentRef, AttachmentSource};
use turnframe_provider::request::ContentPart;
use crate::config::AttachmentConfig;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum NotShown {
Unavailable,
OverBudget,
Unsupported,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OmittedAttachment {
pub attachment_id: AttachmentId,
pub filename: Option<String>,
pub reason: NotShown,
}
#[derive(Debug, Clone, Default)]
pub struct TurnAttachments {
pub parts: Vec<ContentPart>,
pub omitted: Vec<OmittedAttachment>,
}
impl TurnAttachments {
#[must_use]
pub fn is_empty(&self) -> bool {
self.parts.is_empty() && self.omitted.is_empty()
}
pub async fn gather(
source: Option<&Arc<dyn AttachmentSource>>,
config: AttachmentConfig,
turn_id: &TurnId,
attachments: &[AttachmentRef],
carries: impl Fn(&ContentPart) -> bool,
) -> Self {
let Some(source) = source else {
return Self::default();
};
let mut gathered = Self::default();
let mut spent = 0usize;
for attachment in attachments {
if config
.max_files
.is_some_and(|max| gathered.parts.len() >= max)
{
gathered.omit(attachment, NotShown::OverBudget);
continue;
}
let content = match source.fetch(turn_id, attachment).await {
Ok(content) => content,
Err(error) => {
tracing::info!(
target: "turnframe.attachments",
attachment = %attachment.attachment_id,
error = %error,
"an attachment could not be fetched; the turn stands without it"
);
gathered.omit(attachment, NotShown::Unavailable);
continue;
}
};
if config
.max_total_bytes
.is_some_and(|max| spent.saturating_add(content.bytes.len()) > max)
{
gathered.omit(attachment, NotShown::OverBudget);
continue;
}
let part = ContentPart::inline_bytes(content.media_type, &content.bytes);
if !carries(&part) {
gathered.omit(attachment, NotShown::Unsupported);
continue;
}
spent = spent.saturating_add(content.bytes.len());
gathered.parts.push(part);
}
gathered
}
fn omit(&mut self, attachment: &AttachmentRef, reason: NotShown) {
self.omitted.push(OmittedAttachment {
attachment_id: attachment.attachment_id.clone(),
filename: attachment.filename.clone(),
reason,
});
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct AttachmentCopy {
pub unavailable: LocalizedText,
pub over_budget: LocalizedText,
pub unsupported: LocalizedText,
}
impl AttachmentCopy {
#[must_use]
pub fn standard() -> Self {
crate::copy::ServerCopy::translated(Self::english(), "it", ITALIAN)
}
#[must_use]
pub fn english() -> Self {
Self {
unavailable: LocalizedText::new(
"I could not open one of the files you sent, so I have not looked at it.",
),
over_budget: LocalizedText::new(
"One of the files you sent was too large for me to look at.",
),
unsupported: LocalizedText::new("I cannot open files of that kind."),
}
}
#[must_use]
pub fn for_reason(&self, reason: NotShown) -> &LocalizedText {
match reason {
NotShown::Unavailable => &self.unavailable,
NotShown::OverBudget => &self.over_budget,
NotShown::Unsupported => &self.unsupported,
}
}
#[must_use]
pub fn resolved(&self, reason: NotShown, locale: &Locale) -> String {
self.for_reason(reason).resolve(locale).to_owned()
}
}
impl Default for AttachmentCopy {
fn default() -> Self {
Self::standard()
}
}
crate::copy::server_copy!(AttachmentCopy, [unavailable, over_budget, unsupported]);
const ITALIAN: &[(&str, &str)] = &[
(
"unavailable",
"Non ho potuto aprire uno dei file che hai inviato, quindi non l'ho guardato.",
),
(
"over_budget",
"Uno dei file che hai inviato era troppo grande per poterlo guardare.",
),
("unsupported", "Non posso aprire file di quel tipo."),
];