use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; use uuid::Uuid; pub mod bluesky; pub mod discord; pub mod facebook; pub mod instagram_channel; pub mod linkedin; pub mod media_upload; pub mod oauth; pub mod pinterest; pub mod reddit; pub mod snapchat; pub mod telegram_channel; pub mod threads; pub mod tiktok; pub mod twilio_sms; pub mod twitter; pub mod wechat; pub mod youtube; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] #[serde(rename_all = "snake_case")] pub enum ChannelType { Bluesky, Discord, Facebook, Instagram, LinkedIn, Pinterest, Reddit, Snapchat, Telegram, Threads, TikTok, TwilioSms, Twitter, WeChat, WhatsApp, YouTube, } impl std::fmt::Display for ChannelType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Bluesky => write!(f, "bluesky"), Self::Discord => write!(f, "discord"), Self::Facebook => write!(f, "facebook"), Self::Instagram => write!(f, "instagram"), Self::LinkedIn => write!(f, "linkedin"), Self::Pinterest => write!(f, "pinterest"), Self::Reddit => write!(f, "reddit"), Self::Snapchat => write!(f, "snapchat"), Self::Telegram => write!(f, "telegram"), Self::Threads => write!(f, "threads"), Self::TikTok => write!(f, "tiktok"), Self::TwilioSms => write!(f, "twilio_sms"), Self::Twitter => write!(f, "twitter"), Self::WeChat => write!(f, "wechat"), Self::WhatsApp => write!(f, "whatsapp"), Self::YouTube => write!(f, "youtube"), } } } impl std::str::FromStr for ChannelType { type Err = ChannelError; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "bluesky" => Ok(Self::Bluesky), "discord" => Ok(Self::Discord), "facebook" | "fb" => Ok(Self::Facebook), "instagram" | "ig" => Ok(Self::Instagram), "linkedin" => Ok(Self::LinkedIn), "pinterest" => Ok(Self::Pinterest), "reddit" => Ok(Self::Reddit), "snapchat" => Ok(Self::Snapchat), "telegram" | "tg" => Ok(Self::Telegram), "threads" => Ok(Self::Threads), "tiktok" => Ok(Self::TikTok), "twilio" | "twilio_sms" | "sms" => Ok(Self::TwilioSms), "twitter" | "x" => Ok(Self::Twitter), "wechat" => Ok(Self::WeChat), "whatsapp" | "wa" => Ok(Self::WhatsApp), "youtube" | "yt" => Ok(Self::YouTube), _ => Err(ChannelError::UnknownChannel(s.to_string())), } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChannelAccount { pub id: Uuid, pub organization_id: Uuid, pub name: String, pub channel_type: ChannelType, pub credentials: ChannelCredentials, pub settings: ChannelSettings, pub is_active: bool, pub created_at: chrono::DateTime, pub updated_at: chrono::DateTime, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ChannelCredentials { OAuth { access_token: String, refresh_token: Option, expires_at: Option>, scope: Option, }, ApiKey { api_key: String, api_secret: Option, }, UsernamePassword { username: String, password: String, app_password: Option, }, Twilio { account_sid: String, auth_token: String, from_number: String, }, Custom { data: HashMap, }, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct ChannelSettings { pub default_hashtags: Vec, pub auto_shorten_links: bool, pub schedule_enabled: bool, pub timezone: Option, pub rate_limit_per_hour: Option, pub custom: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PostContent { pub text: Option, pub image_urls: Vec, pub video_url: Option, pub link: Option, pub hashtags: Vec, pub mentions: Vec, pub scheduled_at: Option>, pub metadata: HashMap, } impl PostContent { pub fn text(text: impl Into) -> Self { Self { text: Some(text.into()), image_urls: vec![], video_url: None, link: None, hashtags: vec![], mentions: vec![], scheduled_at: None, metadata: HashMap::new(), } } pub fn with_image(mut self, url: impl Into) -> Self { self.image_urls.push(url.into()); self } pub fn with_images(mut self, urls: Vec) -> Self { self.image_urls.extend(urls); self } pub fn with_video(mut self, url: impl Into) -> Self { self.video_url = Some(url.into()); self } pub fn with_link(mut self, url: impl Into) -> Self { self.link = Some(url.into()); self } pub fn with_hashtags(mut self, tags: Vec) -> Self { self.hashtags.extend(tags); self } pub fn with_mentions(mut self, mentions: Vec) -> Self { self.mentions.extend(mentions); self } pub fn scheduled(mut self, at: chrono::DateTime) -> Self { self.scheduled_at = Some(at); self } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PostResult { pub success: bool, pub channel_type: ChannelType, pub post_id: Option, pub url: Option, pub error: Option, pub metadata: HashMap, } impl PostResult { pub fn success(channel_type: ChannelType, post_id: String, url: Option) -> Self { Self { success: true, channel_type, post_id: Some(post_id), url, error: None, metadata: HashMap::new(), } } pub fn error(channel_type: ChannelType, error: impl Into) -> Self { Self { success: false, channel_type, post_id: None, url: None, error: Some(error.into()), metadata: HashMap::new(), } } } #[derive(Debug, Clone)] pub enum ChannelError { UnknownChannel(String), AccountNotFound(String), AuthenticationFailed(String), RateLimited { retry_after: Option }, ContentTooLong { max_length: usize, actual_length: usize }, UnsupportedMediaType(String), NetworkError(String), ApiError { code: Option, message: String }, NotConfigured, } impl std::fmt::Display for ChannelError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::UnknownChannel(name) => write!(f, "Unknown channel: {name}"), Self::AccountNotFound(name) => write!(f, "Account not found: {name}"), Self::AuthenticationFailed(msg) => write!(f, "Authentication failed: {msg}"), Self::RateLimited { retry_after } => { if let Some(secs) = retry_after { write!(f, "Rate limited, retry after {secs} seconds") } else { write!(f, "Rate limited") } } Self::ContentTooLong { max_length, actual_length } => { write!(f, "Content too long: {actual_length} characters (max: {max_length})") } Self::UnsupportedMediaType(media_type) => { write!(f, "Unsupported media type: {media_type}") } Self::NetworkError(msg) => write!(f, "Network error: {msg}"), Self::ApiError { code, message } => { if let Some(c) = code { write!(f, "API error [{c}]: {message}") } else { write!(f, "API error: {message}") } } Self::NotConfigured => write!(f, "Channel not configured"), } } } impl std::error::Error for ChannelError {} #[async_trait::async_trait] pub trait ChannelProvider: Send + Sync { fn channel_type(&self) -> ChannelType; fn max_text_length(&self) -> usize; fn supports_images(&self) -> bool; fn supports_video(&self) -> bool; fn supports_links(&self) -> bool; async fn post(&self, account: &ChannelAccount, content: &PostContent) -> Result; async fn validate_credentials(&self, credentials: &ChannelCredentials) -> Result; async fn refresh_token(&self, account: &mut ChannelAccount) -> Result<(), ChannelError>; } pub struct ChannelManager { accounts: Arc>>, providers: HashMap>, } impl ChannelManager { pub fn new() -> Self { Self { accounts: Arc::new(RwLock::new(HashMap::new())), providers: HashMap::new(), } } pub fn register_provider(&mut self, provider: Arc) { self.providers.insert(provider.channel_type(), provider); } pub async fn add_account(&self, account: ChannelAccount) { let mut accounts = self.accounts.write().await; accounts.insert(account.name.clone(), account); } pub async fn get_account(&self, name: &str) -> Option { let accounts = self.accounts.read().await; accounts.get(name).cloned() } pub async fn remove_account(&self, name: &str) -> Option { let mut accounts = self.accounts.write().await; accounts.remove(name) } pub async fn list_accounts(&self) -> Vec { let accounts = self.accounts.read().await; accounts.values().cloned().collect() } pub async fn post_to( &self, account_name: &str, content: &PostContent, ) -> Result { let account = self .get_account(account_name) .await .ok_or_else(|| ChannelError::AccountNotFound(account_name.to_string()))?; let provider = self .providers .get(&account.channel_type) .ok_or(ChannelError::NotConfigured)?; provider.post(&account, content).await } pub async fn post_to_multiple( &self, account_names: &[String], content: &PostContent, ) -> Vec> { let mut results = Vec::with_capacity(account_names.len()); for name in account_names { let result = self.post_to(name, content).await; results.push(result); } results } pub async fn post_to_channels( &self, channels: &[ChannelType], content: &PostContent, ) -> Vec> { let accounts = self.accounts.read().await; let mut results = Vec::new(); for channel_type in channels { let matching_accounts: Vec<_> = accounts .values() .filter(|a| &a.channel_type == channel_type && a.is_active) .collect(); for account in matching_accounts { if let Some(provider) = self.providers.get(channel_type) { let result = provider.post(account, content).await; results.push(result); } else { results.push(Err(ChannelError::NotConfigured)); } } } results } pub fn get_channel_limits(&self, channel_type: &ChannelType) -> Option { self.providers.get(channel_type).map(|p| ChannelLimits { max_text_length: p.max_text_length(), supports_images: p.supports_images(), supports_video: p.supports_video(), supports_links: p.supports_links(), }) } } impl Default for ChannelManager { fn default() -> Self { Self::new() } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChannelLimits { pub max_text_length: usize, pub supports_images: bool, pub supports_video: bool, pub supports_links: bool, } pub struct MultiPostRequest { pub content: PostContent, pub targets: Vec, } #[derive(Debug, Clone)] pub enum PostTarget { Account(String), Channel(ChannelType), All, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MultiPostResult { pub total: usize, pub successful: usize, pub failed: usize, pub results: Vec, } impl MultiPostResult { pub fn from_results(results: Vec>) -> Self { let total = results.len(); let mut successful = 0; let mut failed = 0; let mut post_results = Vec::with_capacity(total); for result in results { match result { Ok(r) => { if r.success { successful += 1; } else { failed += 1; } post_results.push(r); } Err(e) => { failed += 1; post_results.push(PostResult { success: false, channel_type: ChannelType::Twitter, post_id: None, url: None, error: Some(e.to_string()), metadata: HashMap::new(), }); } } } Self { total, successful, failed, results: post_results, } } }