pub mod bg_resume;
pub mod commands;
mod factory;
pub(crate) mod group_history;
pub mod manager;
pub(crate) mod model_menu;
pub mod question_common;
pub mod session_init;
pub mod session_resolve;
pub mod single_flight;
pub mod target_resolver;
pub(crate) mod transport_ready;
pub(crate) mod typing_tick;
pub mod voice;
#[cfg(feature = "discord")]
pub mod discord;
#[cfg(feature = "slack")]
pub mod slack;
#[cfg(feature = "telegram")]
pub mod telegram;
#[cfg(feature = "trello")]
pub mod trello;
#[cfg(feature = "whatsapp")]
pub mod whatsapp;
mod greeting;
pub use factory::ChannelFactory;
pub use greeting::generate_connection_greeting;
pub use manager::ChannelManager;
pub(crate) fn channel_attachments_dir() -> std::path::PathBuf {
crate::config::opencrabs_home().join("channel_attachments")
}
pub(crate) fn persist_channel_attachment_in(
base: &std::path::Path,
platform: &str,
filename: &str,
bytes: &[u8],
) -> Option<std::path::PathBuf> {
let dir = base.join(platform);
if let Err(e) = std::fs::create_dir_all(&dir) {
tracing::warn!("channel_attachments: mkdir {platform} failed: {e}");
return None;
}
let safe = sanitize_attachment_name(filename);
let millis = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
let path = dir.join(format!("{millis}-{safe}"));
match std::fs::write(&path, bytes) {
Ok(()) => Some(path),
Err(e) => {
tracing::warn!("channel_attachments: write {platform}/{safe} failed: {e}");
None
}
}
}
pub(crate) fn persist_channel_attachment(
platform: &str,
filename: &str,
bytes: &[u8],
) -> Option<std::path::PathBuf> {
persist_channel_attachment_in(&channel_attachments_dir(), platform, filename, bytes)
}
pub(crate) fn sanitize_attachment_name(s: &str) -> String {
let cleaned: String = s
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_') {
c
} else {
'_'
}
})
.collect();
let trimmed = cleaned.trim_start_matches('.');
if trimmed.is_empty() {
"attachment.bin".to_string()
} else {
trimmed.to_string()
}
}