From 57a7b42b9d8abbb2e50a896022892a651c8f7389 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Wed, 20 Aug 2025 16:46:27 +0200 Subject: [PATCH] [refactor] removed `analytics`, `config`, and modular crates to simplify the codebase and streamline architecture --- backend-rust/Cargo.lock | 14 + backend-rust/Cargo.toml | 1 + backend-rust/crates/api/Cargo.toml | 2 + backend-rust/crates/api/src/config.rs | 994 +++++++++++++++- backend-rust/crates/api/src/lib.rs | 3 +- backend-rust/crates/api/src/services.rs | 10 + .../api/src/services/analytics_service.rs | 4 + .../api/src/services/content_processor.rs | 3 + .../crates/api/src/services/news_service.rs | 4 + .../api/src/services/scraping_service.rs | 4 + .../api/src/services/sharing_service.rs | 4 + .../api/src/services/summary_service.rs | 4 + .../api/src/services/tagging_service.rs | 4 + backend-rust/crates/app/Cargo.toml | 14 - backend-rust/crates/app/src/main.rs | 45 - backend-rust/crates/db/Cargo.toml | 10 + .../{src/db.rs => crates/db/src/lib.rs} | 16 +- backend-rust/crates/module-api/Cargo.toml | 12 - backend-rust/crates/module-api/src/lib.rs | 30 - backend-rust/crates/module-host/Cargo.toml | 17 - backend-rust/crates/module-host/src/lib.rs | 114 -- .../crates/modules/summarizer/Cargo.toml | 14 - .../crates/modules/summarizer/src/lib.rs | 50 - backend-rust/crates/server/Cargo.toml | 3 +- backend-rust/crates/server/src/lib.rs | 11 +- backend-rust/src/config.rs | 1003 ----------------- backend-rust/src/main.rs | 104 -- backend-rust/src/models.rs | 6 - backend-rust/src/models/analytics.rs | 0 backend-rust/src/models/article.rs | 0 backend-rust/src/models/settings.rs | 0 backend-rust/src/models/summary.rs | 0 backend-rust/src/models/tag.rs | 0 backend-rust/src/models/user.rs | 0 backend-rust/src/services.rs | 7 - .../src/services/analytics_service.rs | 0 backend-rust/src/services/news_service.rs | 0 backend-rust/src/services/scraping_service.rs | 0 backend-rust/src/services/sharing_service.rs | 0 backend-rust/src/services/summary_service.rs | 0 backend-rust/src/services/tagging_service.rs | 0 41 files changed, 1057 insertions(+), 1450 deletions(-) create mode 100644 backend-rust/crates/api/src/services/analytics_service.rs create mode 100644 backend-rust/crates/api/src/services/content_processor.rs create mode 100644 backend-rust/crates/api/src/services/news_service.rs create mode 100644 backend-rust/crates/api/src/services/scraping_service.rs create mode 100644 backend-rust/crates/api/src/services/sharing_service.rs create mode 100644 backend-rust/crates/api/src/services/summary_service.rs create mode 100644 backend-rust/crates/api/src/services/tagging_service.rs delete mode 100644 backend-rust/crates/app/Cargo.toml delete mode 100644 backend-rust/crates/app/src/main.rs create mode 100644 backend-rust/crates/db/Cargo.toml rename backend-rust/{src/db.rs => crates/db/src/lib.rs} (67%) delete mode 100644 backend-rust/crates/module-api/Cargo.toml delete mode 100644 backend-rust/crates/module-api/src/lib.rs delete mode 100644 backend-rust/crates/module-host/Cargo.toml delete mode 100644 backend-rust/crates/module-host/src/lib.rs delete mode 100644 backend-rust/crates/modules/summarizer/Cargo.toml delete mode 100644 backend-rust/crates/modules/summarizer/src/lib.rs delete mode 100644 backend-rust/src/config.rs delete mode 100644 backend-rust/src/main.rs delete mode 100644 backend-rust/src/models.rs delete mode 100644 backend-rust/src/models/analytics.rs delete mode 100644 backend-rust/src/models/article.rs delete mode 100644 backend-rust/src/models/settings.rs delete mode 100644 backend-rust/src/models/summary.rs delete mode 100644 backend-rust/src/models/tag.rs delete mode 100644 backend-rust/src/models/user.rs delete mode 100644 backend-rust/src/services.rs delete mode 100644 backend-rust/src/services/analytics_service.rs delete mode 100644 backend-rust/src/services/news_service.rs delete mode 100644 backend-rust/src/services/scraping_service.rs delete mode 100644 backend-rust/src/services/sharing_service.rs delete mode 100644 backend-rust/src/services/summary_service.rs delete mode 100644 backend-rust/src/services/tagging_service.rs diff --git a/backend-rust/Cargo.lock b/backend-rust/Cargo.lock index 853d93e..a31358c 100644 --- a/backend-rust/Cargo.lock +++ b/backend-rust/Cargo.lock @@ -59,9 +59,11 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "axum", "once_cell", "serde", "serde_json", + "sqlx", "toml", "tracing", ] @@ -320,6 +322,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "db" +version = "0.1.0" +dependencies = [ + "anyhow", + "api", + "sqlx", + "tracing", +] + [[package]] name = "der" version = "0.7.10" @@ -869,6 +881,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ + "cc", "pkg-config", "vcpkg", ] @@ -1394,6 +1407,7 @@ dependencies = [ "anyhow", "api", "axum", + "db", "dotenv", "http", "once_cell", diff --git a/backend-rust/Cargo.toml b/backend-rust/Cargo.toml index 4695164..10a491b 100644 --- a/backend-rust/Cargo.toml +++ b/backend-rust/Cargo.toml @@ -3,6 +3,7 @@ members = [ "crates/api", "crates/server", "crates/cli", + "crates/db", ] resolver = "3" diff --git a/backend-rust/crates/api/Cargo.toml b/backend-rust/crates/api/Cargo.toml index 08de31a..71af305 100644 --- a/backend-rust/crates/api/Cargo.toml +++ b/backend-rust/crates/api/Cargo.toml @@ -11,6 +11,8 @@ once_cell = { workspace = true } toml = { workspace = true } tracing = { workspace = true } async-trait = "0.1.89" +axum = { workspace = true } +sqlx = { workspace = true, features = ["sqlite"] } [features] default = [] diff --git a/backend-rust/crates/api/src/config.rs b/backend-rust/crates/api/src/config.rs index 08c1926..2684b30 100644 --- a/backend-rust/crates/api/src/config.rs +++ b/backend-rust/crates/api/src/config.rs @@ -1,4 +1,477 @@ use serde::{Deserialize, Serialize}; +use std::env; +use std::path::PathBuf; + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(default)] +pub struct ConfigFile { + #[serde(default)] + pub server: Server, + #[serde(default)] + pub display: ConfDisplay, + #[serde(default)] + pub analytics: Analytics, + #[serde(default)] + pub filtering: Filtering, + #[serde(default)] + pub sharing: Sharing, + #[serde(default)] + pub ai: Ai, + #[serde(default)] + pub scraping: Scraping, + #[serde(default)] + pub processing: Processing, + #[serde(default)] + pub migration: ConfMigration, + #[serde(default)] + pub cli: Cli, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(default)] +pub struct ContentQuality { + pub min_content_length: u32, + pub max_content_length: u32, + pub min_text_html_ratio: f32, + pub readability_threshold: f32, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(default)] +pub struct DuplicateDetection { + pub enabled: bool, + pub title_similarity_threshold: f32, + pub content_similarity_threshold: f32, + pub check_historical_days: u32, + pub store_fingerprints: bool, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(default)] +pub struct AdPrevention { + pub enabled: bool, + pub block_iframes: bool, + pub clean_content: bool, + pub ad_patterns: Vec, + pub preserved_elements: Vec, + pub removed_elements: Vec, +} + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +pub enum DefaultView { + Compact, + Full, + Summary, +} + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +pub enum Theme { + Light, + Dark, + Auto, +} + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +pub enum DefaultSort { + #[serde(rename = "added_desc")] + AddedDesc, + #[serde(rename = "published_desc")] + PublishedDesc, + #[serde(rename = "title_asc")] + TitleAsc, +} + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +pub enum DefaultFormat { + #[serde(rename = "text")] + Text, + #[serde(rename = "markdown")] + Markdown, + #[serde(rename = "json")] + JSON, + #[serde(rename = "html")] + HTML, +} + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +pub enum AiProvider { + #[serde(rename = "ollama")] + Ollama, + #[serde(rename = "openai")] + OpenAi, + #[serde(rename = "anthropic")] + Anthropic, +} + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +pub enum DefaultOutput { + #[serde(rename = "table")] + Table, + #[serde(rename = "json")] + JSON, + #[serde(rename = "csv")] + CSV, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(default)] +pub struct Server { + #[serde(default = "Server::default_host")] + pub host: String, + #[serde(default = "Server::default_port")] + pub port: u16, +} +impl Server { + fn default_host() -> String { + Server::default().host + } + fn default_port() -> u16 { + Server::default().port + } +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(default)] +pub struct ConfDisplay { + #[serde(default)] + pub default_view: DefaultView, + #[serde(default = "ConfDisplay::default_articles_per_page")] + pub articles_per_page: u32, + #[serde(default = "ConfDisplay::default_show_reading_time")] + pub show_reading_time: bool, + #[serde(default = "ConfDisplay::default_show_word_count")] + pub show_word_count: bool, + #[serde(default = "ConfDisplay::default_highlight_unread")] + pub highlight_unread: bool, + #[serde(default)] + pub theme: Theme, +} +impl ConfDisplay { + fn default_articles_per_page() -> u32 { + ConfDisplay::default().articles_per_page + } + fn default_show_reading_time() -> bool { + ConfDisplay::default().show_reading_time + } + fn default_show_word_count() -> bool { + ConfDisplay::default().show_word_count + } + fn default_highlight_unread() -> bool { + ConfDisplay::default().highlight_unread + } +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(default)] +pub struct Analytics { + #[serde(default = "Analytics::default_enabled")] + pub enabled: bool, + #[serde(default = "Analytics::default_track_reading_time")] + pub track_reading_time: bool, + #[serde(default = "Analytics::default_track_scroll_position")] + pub track_scroll_position: bool, + #[serde(default = "Analytics::default_retention_days")] + pub retention_days: u32, + #[serde(default = "Analytics::default_aggregate_older_data")] + pub aggregate_older_data: bool, +} +impl Analytics { + fn default_enabled() -> bool { + Analytics::default().enabled + } + fn default_track_reading_time() -> bool { + Analytics::default().track_reading_time + } + fn default_track_scroll_position() -> bool { + Analytics::default().track_scroll_position + } + fn default_retention_days() -> u32 { + Analytics::default().retention_days + } + fn default_aggregate_older_data() -> bool { + Analytics::default().aggregate_older_data + } +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(default)] +pub struct Filtering { + #[serde(default = "Filtering::default_enable_smart_suggestions")] + pub enable_smart_suggestions: bool, + #[serde(default = "Filtering::default_max_recent_filters")] + pub max_recent_filters: u32, + #[serde(default = "Filtering::default_auto_save_filters")] + pub auto_save_filters: bool, + #[serde(default)] + pub default_sort: DefaultSort, + #[serde(default = "Filtering::default_enable_geographic_hierarchy")] + pub enable_geographic_hierarchy: bool, + #[serde(default = "Filtering::default_auto_migrate_country_filters")] + pub auto_migrate_country_filters: bool, +} +impl Filtering { + fn default_enable_smart_suggestions() -> bool { + Filtering::default().enable_smart_suggestions + } + fn default_max_recent_filters() -> u32 { + Filtering::default().max_recent_filters + } + fn default_auto_save_filters() -> bool { + Filtering::default().auto_save_filters + } + fn default_enable_geographic_hierarchy() -> bool { + Filtering::default().enable_geographic_hierarchy + } + fn default_auto_migrate_country_filters() -> bool { + Filtering::default().auto_migrate_country_filters + } +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(default)] +pub struct Sharing { + #[serde(default)] + pub default_format: DefaultFormat, + #[serde(default = "Sharing::default_include_summary")] + pub include_summary: bool, + #[serde(default = "Sharing::default_include_tags")] + pub include_tags: bool, + #[serde(default = "Sharing::default_include_source")] + pub include_source: bool, + #[serde(default = "Sharing::default_copy_to_clipboard")] + pub copy_to_clipboard: bool, + #[serde(default)] + pub templates: SharingTemplates, +} +impl Sharing { + fn default_include_summary() -> bool { + Sharing::default().include_summary + } + fn default_include_tags() -> bool { + Sharing::default().include_tags + } + fn default_include_source() -> bool { + Sharing::default().include_source + } + fn default_copy_to_clipboard() -> bool { + Sharing::default().copy_to_clipboard + } +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(default)] +pub struct SharingTemplates { + #[serde(default = "SharingTemplate::default_text")] + pub text: SharingTemplate, + #[serde(default = "SharingTemplate::default_markdown")] + pub markdown: SharingTemplate, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(default)] +pub struct SharingTemplate { + pub format: String, +} +impl Default for SharingTemplate { + fn default() -> Self { + // Fallback empty only if used generically; specific fields use the richer defaults below. + Self { + format: String::new(), + } + } +} +impl SharingTemplate { + pub fn default_text() -> Self { + SharingTemplates::default().text + } + pub fn default_markdown() -> Self { + SharingTemplates::default().markdown + } +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(default)] +pub struct Ai { + #[serde(default = "Ai::default_enabled")] + pub enabled: bool, + #[serde(default)] + pub provider: AiProvider, + #[serde(default = "Ai::default_timeout_seconds")] + pub timeout_seconds: u32, + #[serde(default)] + pub summary: AiSummary, + #[serde(default)] + pub tagging: AiTagging, +} +impl Ai { + fn default_enabled() -> bool { + Ai::default().enabled + } + fn default_timeout_seconds() -> u32 { + Ai::default().timeout_seconds + } +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(default)] +pub struct AiSummary { + #[serde(default = "AiSummary::default_enabled")] + pub enabled: bool, + #[serde(default = "AiSummary::default_temperature")] + pub temperature: f32, + #[serde(default = "AiSummary::default_max_tokens")] + pub max_tokens: u32, +} +impl AiSummary { + fn default_enabled() -> bool { + AiSummary::default().enabled + } + fn default_temperature() -> f32 { + AiSummary::default().temperature + } + fn default_max_tokens() -> u32 { + AiSummary::default().max_tokens + } +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(default)] +pub struct AiTagging { + #[serde(default = "AiTagging::default_enabled")] + pub enabled: bool, + #[serde(default = "AiTagging::default_temperature")] + pub temperature: f32, + #[serde(default = "AiTagging::default_max_tokens")] + pub max_tokens: u32, + #[serde(default = "AiTagging::default_max_tags_per_article")] + pub max_tags_per_article: u32, + #[serde(default = "AiTagging::default_min_confidence_threshold")] + pub min_confidence_threshold: f32, + #[serde(default = "AiTagging::default_enable_geographic_tagging")] + pub enable_geographic_tagging: bool, + #[serde(default = "AiTagging::default_enable_category_tagging")] + pub enable_category_tagging: bool, + #[serde(default = "AiTagging::default_geographic_hierarchy_levels")] + pub geographic_hierarchy_levels: u32, +} +impl AiTagging { + fn default_enabled() -> bool { + AiTagging::default().enabled + } + fn default_temperature() -> f32 { + AiTagging::default().temperature + } + fn default_max_tokens() -> u32 { + AiTagging::default().max_tokens + } + fn default_max_tags_per_article() -> u32 { + AiTagging::default().max_tags_per_article + } + fn default_min_confidence_threshold() -> f32 { + AiTagging::default().min_confidence_threshold + } + fn default_enable_geographic_tagging() -> bool { + AiTagging::default().enable_geographic_tagging + } + fn default_enable_category_tagging() -> bool { + AiTagging::default().enable_category_tagging + } + fn default_geographic_hierarchy_levels() -> u32 { + AiTagging::default().geographic_hierarchy_levels + } +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(default)] +pub struct Scraping { + #[serde(default = "Scraping::default_timeout_seconds")] + pub timeout_seconds: u32, + #[serde(default = "Scraping::default_max_retries")] + pub max_retries: u32, + #[serde(default = "Scraping::default_max_content_length")] + pub max_content_length: u32, + #[serde(default = "Scraping::default_respect_robots_txt")] + pub respect_robots_txt: bool, + #[serde(default = "Scraping::default_rate_limit_delay_ms")] + pub rate_limit_delay_ms: u32, + #[serde(default)] + pub content_quality: ContentQuality, + #[serde(default)] + pub duplicate_detection: DuplicateDetection, + #[serde(default)] + pub ad_prevention: AdPrevention, +} +impl Scraping { + fn default_timeout_seconds() -> u32 { + Scraping::default().timeout_seconds + } + fn default_max_retries() -> u32 { + Scraping::default().max_retries + } + fn default_max_content_length() -> u32 { + Scraping::default().max_content_length + } + fn default_respect_robots_txt() -> bool { + Scraping::default().respect_robots_txt + } + fn default_rate_limit_delay_ms() -> u32 { + Scraping::default().rate_limit_delay_ms + } +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(default)] +pub struct Processing { + #[serde(default = "Processing::default_batch_size")] + pub batch_size: u32, + #[serde(default = "Processing::default_max_concurrent")] + pub max_concurrent: u32, + #[serde(default = "Processing::default_retry_attempts")] + pub retry_attempts: u32, + #[serde(default = "Processing::default_priority_manual")] + pub priority_manual: bool, + #[serde(default = "Processing::default_auto_mark_read_on_view")] + pub auto_mark_read_on_view: bool, +} +impl Processing { + fn default_batch_size() -> u32 { + Processing::default().batch_size + } + fn default_max_concurrent() -> u32 { + Processing::default().max_concurrent + } + fn default_retry_attempts() -> u32 { + Processing::default().retry_attempts + } + fn default_priority_manual() -> bool { + Processing::default().priority_manual + } + fn default_auto_mark_read_on_view() -> bool { + Processing::default().auto_mark_read_on_view + } +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(default)] +pub struct ConfMigration { + #[serde(default = "ConfMigration::default_auto_convert_country_filters")] + pub auto_convert_country_filters: bool, + #[serde(default = "ConfMigration::default_preserve_legacy_data")] + pub preserve_legacy_data: bool, + #[serde(default = "ConfMigration::default_migration_batch_size")] + pub migration_batch_size: u32, +} +impl ConfMigration { + fn default_auto_convert_country_filters() -> bool { + ConfMigration::default().auto_convert_country_filters + } + fn default_preserve_legacy_data() -> bool { + ConfMigration::default().preserve_legacy_data + } + fn default_migration_batch_size() -> u32 { + ConfMigration::default().migration_batch_size + } +} #[derive(Deserialize, Serialize, Debug, Clone)] #[serde(default)] @@ -14,24 +487,208 @@ pub struct Cli { #[serde(default = "Cli::default_show_geographic_hierarchy")] pub show_geographic_hierarchy: bool, } +impl Cli { + fn default_pager_command() -> String { + Cli::default().pager_command + } + fn default_show_progress() -> bool { + Cli::default().show_progress + } + fn default_auto_confirm_bulk() -> bool { + Cli::default().auto_confirm_bulk + } + fn default_show_geographic_hierarchy() -> bool { + Cli::default().show_geographic_hierarchy + } +} -impl Default for Cli { +impl Default for ContentQuality { fn default() -> Self { Self { - default_output: DefaultOutput::Table, - pager_command: Self::default_pager_command(), - show_progress: Self::default_show_progress(), - auto_confirm_bulk: Self::default_auto_confirm_bulk(), - show_geographic_hierarchy: Self::default_show_geographic_hierarchy(), + min_content_length: 100, + max_content_length: 500_000, + min_text_html_ratio: 0.3, + readability_threshold: 0.6, } } } -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(rename_all = "snake_case")] -pub enum DefaultOutput { - Table, - Json, +impl Default for DuplicateDetection { + fn default() -> Self { + Self { + enabled: true, + title_similarity_threshold: 0.90, + content_similarity_threshold: 0.85, + check_historical_days: 30, + store_fingerprints: true, + } + } +} + +impl Default for AdPrevention { + fn default() -> Self { + Self { + enabled: true, + block_iframes: true, + clean_content: true, + ad_patterns: vec![ + "sponsored-content".to_string(), + "advertisement".to_string(), + "promoted-post".to_string(), + "partner-content".to_string(), + "ad-wrapper".to_string(), + "sponsored".to_string(), + ], + preserved_elements: vec![ + "article".to_string(), + "p".to_string(), + "h1".to_string(), + "h2".to_string(), + "h3".to_string(), + "img".to_string(), + "figure".to_string(), + "figcaption".to_string(), + ], + removed_elements: vec![ + "aside".to_string(), + "iframe".to_string(), + "script".to_string(), + "style".to_string(), + "ins".to_string(), + "form".to_string(), + "button".to_string(), + ".ad".to_string(), + "#ad".to_string(), + "[class*='ad-']".to_string(), + "[id*='ad-']".to_string(), + ], + } + } +} + +impl DefaultView { + pub fn display_name(&self) -> &str { + match self { + DefaultView::Compact => "Compact", + DefaultView::Full => "Full Article", + DefaultView::Summary => "Summary", + } + } + + pub fn from_string(s: &str) -> Option { + match s.to_lowercase().as_str() { + "compact" => Some(DefaultView::Compact), + "full" => Some(DefaultView::Full), + "summary" => Some(DefaultView::Summary), + _ => None, + } + } +} + +impl Default for DefaultView { + fn default() -> Self { + DefaultView::Compact + } +} + +impl Theme { + pub fn display_name(&self) -> &str { + match self { + Theme::Light => "Light Mode", + Theme::Dark => "Dark Mode", + Theme::Auto => "Auto (System)", + } + } + + pub fn is_dark_mode(&self, system_prefers_dark: bool) -> bool { + match self { + Theme::Light => false, + Theme::Dark => true, + Theme::Auto => system_prefers_dark, + } + } +} + +impl Default for Theme { + fn default() -> Self { + Theme::Auto + } +} + +impl DefaultSort { + pub fn display_name(&self) -> &str { + match self { + DefaultSort::AddedDesc => "Recently Added", + DefaultSort::PublishedDesc => "Recently Published", + DefaultSort::TitleAsc => "Title A-Z", + } + } + + pub fn to_sql_order(&self) -> &str { + match self { + DefaultSort::AddedDesc => "added_at DESC", + DefaultSort::PublishedDesc => "published_at DESC", + DefaultSort::TitleAsc => "title ASC", + } + } +} + +impl Default for DefaultSort { + fn default() -> Self { + DefaultSort::AddedDesc + } +} + +impl DefaultFormat { + pub fn display_name(&self) -> &str { + match self { + DefaultFormat::Text => "Plain Text", + DefaultFormat::Markdown => "Markdown", + DefaultFormat::JSON => "JSON", + DefaultFormat::HTML => "HTML", + } + } + + pub fn mime_type(&self) -> &str { + match self { + DefaultFormat::Text => "text/plain", + DefaultFormat::Markdown => "text/markdown", + DefaultFormat::JSON => "application/json", + DefaultFormat::HTML => "text/html", + } + } +} + +impl Default for DefaultFormat { + fn default() -> Self { + DefaultFormat::Text + } +} + +impl AiProvider { + pub fn display_name(&self) -> &str { + match self { + AiProvider::Ollama => "Ollama", + AiProvider::OpenAi => "OpenAI", + AiProvider::Anthropic => "Anthropic", + } + } +} + +impl Default for AiProvider { + fn default() -> Self { + AiProvider::Ollama + } +} + +impl DefaultOutput { + pub fn display_name(&self) -> &str { + match self { + DefaultOutput::Table => "Table", + DefaultOutput::JSON => "JSON", + DefaultOutput::CSV => "CSV", + } + } } impl Default for DefaultOutput { @@ -40,18 +697,307 @@ impl Default for DefaultOutput { } } -impl Cli { - pub fn default_pager_command() -> String { - // Example default; customize as needed - "less -R".to_string() - } - pub fn default_show_progress() -> bool { - true - } - pub fn default_auto_confirm_bulk() -> bool { - false - } - pub fn default_show_geographic_hierarchy() -> bool { - true +impl Default for Cli { + fn default() -> Self { + Self { + default_output: DefaultOutput::Table, + pager_command: "less -R".to_string(), + show_progress: true, + auto_confirm_bulk: false, + show_geographic_hierarchy: true, + } + } +} + +impl Default for ConfMigration { + fn default() -> Self { + Self { + auto_convert_country_filters: true, + preserve_legacy_data: true, + migration_batch_size: 100, + } + } +} + +impl Default for Processing { + fn default() -> Self { + Self { + batch_size: 10, + max_concurrent: 5, + retry_attempts: 3, + priority_manual: true, + auto_mark_read_on_view: false, + } + } +} + +impl Default for Scraping { + fn default() -> Self { + Self { + timeout_seconds: 30, + max_retries: 3, + max_content_length: 100_000, + respect_robots_txt: true, + rate_limit_delay_ms: 1000, + content_quality: ContentQuality::default(), + duplicate_detection: DuplicateDetection::default(), + ad_prevention: AdPrevention::default(), + } + } +} + +impl Default for Ai { + fn default() -> Self { + Self { + enabled: true, + provider: AiProvider::default(), + timeout_seconds: 120, + summary: AiSummary::default(), + tagging: AiTagging::default(), + } + } +} + +impl Default for AiSummary { + fn default() -> Self { + Self { + enabled: true, + temperature: 0.1, + max_tokens: 1024, + } + } +} + +impl Default for AiTagging { + fn default() -> Self { + Self { + enabled: true, + temperature: 0.3, + max_tokens: 200, + max_tags_per_article: 10, + min_confidence_threshold: 0.7, + enable_geographic_tagging: true, + enable_category_tagging: true, + geographic_hierarchy_levels: 3, + } + } +} + +impl Default for Sharing { + fn default() -> Self { + Self { + default_format: DefaultFormat::default(), + include_summary: true, + include_tags: true, + include_source: true, + copy_to_clipboard: true, + templates: SharingTemplates::default(), + } + } +} + +impl Default for SharingTemplates { + fn default() -> Self { + Self { + text: SharingTemplate { + format: r#"📰 {title} + +{summary} + +🏷️ Tags: {tags} +🌍 Location: {geographic_tags} +🔗 Source: {url} +📅 Published: {published_at} + +Shared via Owly News Summariser +"# + .to_string(), + }, + markdown: SharingTemplate { + format: r#"# {title} + +{summary} + +**Tags:** {tags} +**Location:** {geographic_tags} +**Source:** [{url}]({url}) +**Published:** {published_at} + +--- +*Shared via Owly News Summariser* +"# + .to_string(), + }, + } + } +} + +impl Default for Filtering { + fn default() -> Self { + Self { + enable_smart_suggestions: true, + max_recent_filters: 10, + auto_save_filters: true, + default_sort: DefaultSort::AddedDesc, + enable_geographic_hierarchy: true, + auto_migrate_country_filters: true, + } + } +} + +impl Default for Analytics { + fn default() -> Self { + Self { + enabled: true, + track_reading_time: true, + track_scroll_position: true, + retention_days: 365, + aggregate_older_data: true, + } + } +} + +impl Default for ConfDisplay { + fn default() -> Self { + Self { + default_view: DefaultView::default(), + articles_per_page: 50, + show_reading_time: true, + show_word_count: false, + highlight_unread: true, + theme: Theme::default(), + } + } +} + +impl Default for Server { + fn default() -> Self { + Self { + host: "127.0.0.1".to_string(), + port: 8090, + } + } +} +impl Default for ConfigFile { + fn default() -> Self { + Self { + server: Server::default(), + display: ConfDisplay::default(), + analytics: Analytics::default(), + filtering: Filtering::default(), + sharing: Sharing::default(), + ai: Ai::default(), + scraping: Scraping::default(), + processing: Processing::default(), + migration: ConfMigration::default(), + cli: Cli::default(), + } + } +} + +impl ConfigFile { + pub fn load_from_file(app_settings: &AppSettings) -> Result> { + let config_path_str = &app_settings.config_path; + let config_path = PathBuf::from(config_path_str); + + if !config_path.exists() { + let config_file = ConfigFile::default(); + config_file.save_to_file(app_settings)?; + return Ok(config_file); + } + + let contents = std::fs::read_to_string(config_path_str)?; + let config: ConfigFile = toml::from_str(&contents)?; + Ok(config) + } + + pub fn save_to_file( + &self, + app_settings: &AppSettings, + ) -> Result<(), Box> { + let contents = toml::to_string_pretty(self)?; + std::fs::write(&app_settings.config_path, contents)?; + Ok(()) + } +} + +#[derive(Deserialize, Debug)] +pub struct AppSettings { + pub config_path: String, + pub db_path: String, + pub migration_path: String, + pub config: ConfigFile, +} + +impl Default for AppSettings { + fn default() -> Self { + Self { + config_path: Self::get_config_path(), + db_path: Self::get_db_path(), + migration_path: Self::get_migration_path(), + config: ConfigFile::default(), + } + } +} + +impl AppSettings { + const FALLBACK_DIR: &'static str = "/tmp"; + const PROD_DB_PATH: &'static str = "/var/lib/owly-news/owlynews.sqlite3"; + const PROD_MIGRATION_PATH: &'static str = "/usr/share/owly-news/migrations"; + + pub fn get_app_settings() -> Self { + AppSettings::default() + } + + fn get_base_path(env_var: &str, fallback: &str) -> String { + env::var(env_var).unwrap_or_else(|_| fallback.to_string()) + } + + fn get_db_path() -> String { + if cfg!(debug_assertions) { + let manifest_dir = Self::get_base_path("CARGO_MANIFEST_DIR", Self::FALLBACK_DIR); + format!("{}/owlynews.sqlite3", manifest_dir) + } else { + Self::PROD_DB_PATH.to_string() + } + } + + fn get_config_path() -> String { + if cfg!(debug_assertions) { + let manifest_dir = Self::get_base_path("CARGO_MANIFEST_DIR", Self::FALLBACK_DIR); + format!("{}/config.toml", manifest_dir) + } else { + let home = Self::get_base_path("HOME", Self::FALLBACK_DIR); + format!("{}/.config/owly-news/config.toml", home) + } + } + + fn get_migration_path() -> String { + if cfg!(debug_assertions) { + let manifest_dir = Self::get_base_path("CARGO_MANIFEST_DIR", Self::FALLBACK_DIR); + format!("{}/migrations", manifest_dir) + } else { + Self::PROD_MIGRATION_PATH.to_string() + } + } + + pub fn database_url(&self) -> String { + format!("sqlite:{}", self.db_path) + } + + pub fn ensure_default_directory(&self) -> Result<(), std::io::Error> { + // Always create the database directory + if let Some(parent) = std::path::Path::new(&self.db_path).parent() { + std::fs::create_dir_all(parent)?; + } + + // In production mode, also create the config directory + if !cfg!(debug_assertions) { + if let Some(parent) = std::path::Path::new(&self.config_path).parent() { + std::fs::create_dir_all(parent)?; + } + } + + Ok(()) } } diff --git a/backend-rust/crates/api/src/lib.rs b/backend-rust/crates/api/src/lib.rs index 55dd718..ef36f3b 100644 --- a/backend-rust/crates/api/src/lib.rs +++ b/backend-rust/crates/api/src/lib.rs @@ -2,4 +2,5 @@ pub mod config; pub mod types; -pub mod services; \ No newline at end of file +pub mod services; +pub mod api; diff --git a/backend-rust/crates/api/src/services.rs b/backend-rust/crates/api/src/services.rs index bdb17ce..25b60ff 100644 --- a/backend-rust/crates/api/src/services.rs +++ b/backend-rust/crates/api/src/services.rs @@ -1,6 +1,16 @@ use crate::types::Health; use async_trait::async_trait; +// Submodules that host various domain services. These were refactored from the +// legacy root src folder into this workspace crate. Each component is its own module file. +pub mod summary_service; +pub mod news_service; +pub mod scraping_service; +pub mod tagging_service; +pub mod analytics_service; +pub mod sharing_service; +pub(crate) mod content_processor; + // Implement your service traits here. Example: #[async_trait] pub trait HealthService: Send + Sync { diff --git a/backend-rust/crates/api/src/services/analytics_service.rs b/backend-rust/crates/api/src/services/analytics_service.rs new file mode 100644 index 0000000..770ee61 --- /dev/null +++ b/backend-rust/crates/api/src/services/analytics_service.rs @@ -0,0 +1,4 @@ +//! Analytics service module. +//! Implement logic for tracking and aggregating analytics here. + +// Placeholder for analytics-related types and functions. diff --git a/backend-rust/crates/api/src/services/content_processor.rs b/backend-rust/crates/api/src/services/content_processor.rs new file mode 100644 index 0000000..9a8f3b0 --- /dev/null +++ b/backend-rust/crates/api/src/services/content_processor.rs @@ -0,0 +1,3 @@ +//! Content processor utilities shared by services. + +// Placeholder module for content processing helpers (e.g., cleaning, tokenization). diff --git a/backend-rust/crates/api/src/services/news_service.rs b/backend-rust/crates/api/src/services/news_service.rs new file mode 100644 index 0000000..5c0b60a --- /dev/null +++ b/backend-rust/crates/api/src/services/news_service.rs @@ -0,0 +1,4 @@ +//! News service module. +//! Implement logic related to news retrieval/management here. + +// Placeholder for news-related types and functions. diff --git a/backend-rust/crates/api/src/services/scraping_service.rs b/backend-rust/crates/api/src/services/scraping_service.rs new file mode 100644 index 0000000..6ac3ba2 --- /dev/null +++ b/backend-rust/crates/api/src/services/scraping_service.rs @@ -0,0 +1,4 @@ +//! Scraping service module. +//! Implement logic related to web scraping, fetchers, and extractors here. + +// Placeholder for scraping-related types and functions. diff --git a/backend-rust/crates/api/src/services/sharing_service.rs b/backend-rust/crates/api/src/services/sharing_service.rs new file mode 100644 index 0000000..52962e5 --- /dev/null +++ b/backend-rust/crates/api/src/services/sharing_service.rs @@ -0,0 +1,4 @@ +//! Sharing service module. +//! Implement logic related to content sharing here. + +// Placeholder for sharing-related types and functions. diff --git a/backend-rust/crates/api/src/services/summary_service.rs b/backend-rust/crates/api/src/services/summary_service.rs new file mode 100644 index 0000000..c6c96f7 --- /dev/null +++ b/backend-rust/crates/api/src/services/summary_service.rs @@ -0,0 +1,4 @@ +//! Summary service module. +//! Implement logic for generating summaries from articles here. + +// Placeholder for summary-related types and functions. diff --git a/backend-rust/crates/api/src/services/tagging_service.rs b/backend-rust/crates/api/src/services/tagging_service.rs new file mode 100644 index 0000000..6cf86f9 --- /dev/null +++ b/backend-rust/crates/api/src/services/tagging_service.rs @@ -0,0 +1,4 @@ +//! Tagging service module. +//! Implement logic related to tagging articles and managing tags here. + +// Placeholder for tagging-related types and functions. diff --git a/backend-rust/crates/app/Cargo.toml b/backend-rust/crates/app/Cargo.toml deleted file mode 100644 index 0f88f4e..0000000 --- a/backend-rust/crates/app/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "owly-news" -version.workspace = true -edition.workspace = true - -[dependencies] -owly-news-api = { path = "../api" } -owly-news-module-host = { path = "../module-host" } -tokio = { workspace = true, features = ["rt-multi-thread", "macros", "sync"] } -tracing = { workspace = true } -tracing-subscriber = { workspace = true, features = ["env-filter", "json"] } -anyhow = { workspace = true } -serde_json = { workspace = true } -num_cpus = { workspace = true } diff --git a/backend-rust/crates/app/src/main.rs b/backend-rust/crates/app/src/main.rs deleted file mode 100644 index 50f9c30..0000000 --- a/backend-rust/crates/app/src/main.rs +++ /dev/null @@ -1,45 +0,0 @@ -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; - -#[tokio::main(flavor = "multi_thread")] -async fn main() -> anyhow::Result<()> { - // Tracing setup - tracing_subscriber::registry() - .with( - tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| "info".into()), - ) - .with(tracing_subscriber::fmt::layer()) - .init(); - - // Limit worker threads for CPU control (can be tuned via env) - // Note: When using #[tokio::main], configure via env TOKIO_WORKER_THREADS. - // Alternatively, build a Runtime manually for stricter control. - if let Ok(threads) = std::env::var("TOKIO_WORKER_THREADS") { - tracing::warn!( - "TOKIO_WORKER_THREADS is set to {threads}, ensure it matches deployment requirements" - ); - } else { - // Provide a sane default via env if not set - let default_threads = std::cmp::max(1, num_cpus::get_physical() / 2); - unsafe { std::env::set_var("TOKIO_WORKER_THREADS", default_threads.to_string()); } - tracing::info!("Defaulting worker threads to {}", default_threads); - } - - // Example: lazily load and invoke the "summarizer" module when needed - let host = owly_news_module_host::ModuleHost::default(); - - // Simulate an on-demand call (e.g., from an HTTP handler) - let summarizer = host.get("summarizer").await?; - let resp = summarizer.invoke_json( - "summarize", - serde_json::json!({ - "text": "Rust enables fearless concurrency with strong guarantees over memory safety.", - "ratio": 0.3 - }), - )?; - tracing::info!(?resp, "summarizer response"); - - // TODO: wire this into your API routes/handlers, using the host.get("").await when needed. - tracing::info!("owly-news daemon running"); - Ok(()) -} diff --git a/backend-rust/crates/db/Cargo.toml b/backend-rust/crates/db/Cargo.toml new file mode 100644 index 0000000..0d01517 --- /dev/null +++ b/backend-rust/crates/db/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "db" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = { workspace = true } +sqlx = { workspace = true, features = ["sqlite"] } +tracing = { workspace = true } +api = { path = "../api" } diff --git a/backend-rust/src/db.rs b/backend-rust/crates/db/src/lib.rs similarity index 67% rename from backend-rust/src/db.rs rename to backend-rust/crates/db/src/lib.rs index 93151c3..dcce449 100644 --- a/backend-rust/src/db.rs +++ b/backend-rust/crates/db/src/lib.rs @@ -1,4 +1,4 @@ -use crate::config::AppSettings; +use api::config::AppSettings; use anyhow::{Context, Result}; use sqlx::migrate::Migrator; use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; @@ -7,10 +7,14 @@ use std::str::FromStr; use std::time::Duration; use tracing::info; -pub const MIGRATOR: Migrator = sqlx::migrate!("./migrations"); +// Embed migrations from the workspace-level migrations directory. +// crates/db is two levels below backend-rust where migrations/ resides. +pub const MIGRATOR: Migrator = sqlx::migrate!("../../migrations"); pub async fn initialize_db(app_settings: &AppSettings) -> Result> { - app_settings.ensure_default_directory()?; + app_settings + .ensure_default_directory() + .context("Failed to ensure default directory for database")?; let options = SqliteConnectOptions::from_str(&app_settings.database_url())? .create_if_missing(true) @@ -25,7 +29,10 @@ pub async fn initialize_db(app_settings: &AppSettings) -> Result> { .connect_with(options) .await?; - MIGRATOR.run(&pool).await.with_context(|| "Database migrations failed")?; + MIGRATOR + .run(&pool) + .await + .with_context(|| "Database migrations failed")?; info!("Database migrations completed successfully"); Ok(pool) @@ -33,6 +40,5 @@ pub async fn initialize_db(app_settings: &AppSettings) -> Result> { pub async fn create_pool(opts: SqliteConnectOptions) -> Result { let pool = SqlitePool::connect_with(opts).await?; - Ok(pool) } diff --git a/backend-rust/crates/module-api/Cargo.toml b/backend-rust/crates/module-api/Cargo.toml deleted file mode 100644 index 5fd31d3..0000000 --- a/backend-rust/crates/module-api/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "owly-news-module-api" -version.workspace = true -edition.workspace = true - -[lib] -path = "src/lib.rs" - -[dependencies] -anyhow = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } diff --git a/backend-rust/crates/module-api/src/lib.rs b/backend-rust/crates/module-api/src/lib.rs deleted file mode 100644 index aa9c52f..0000000 --- a/backend-rust/crates/module-api/src/lib.rs +++ /dev/null @@ -1,30 +0,0 @@ -use std::ffi::{CStr, CString}; -use std::os::raw::c_char; - -// Symbols every module must export with `extern "C"` and `#[no_mangle]`. -// Signature: fn module_name() -> *const c_char -// Signature: fn module_invoke(op: *const c_char, payload: *const c_char) -> *mut c_char -pub const SYMBOL_NAME: &str = "module_name"; -pub const SYMBOL_INVOKE: &str = "module_invoke"; - -// Helper to convert C char* to &str -pub unsafe fn cstr_to_str<'a>(ptr: *const c_char) -> anyhow::Result<&'a str> { - if ptr.is_null() { - anyhow::bail!("null pointer"); - } - Ok(CStr::from_ptr(ptr).to_str()?) -} - -// Helper to allocate a CString for return across FFI boundary (module side) -pub fn string_to_cstring_ptr(s: String) -> *mut c_char { - CString::new(s).unwrap().into_raw() -} - -// Helper to take back ownership of a CString (host side), then free by letting CString drop -pub unsafe fn take_cstring(ptr: *mut c_char) -> anyhow::Result { - if ptr.is_null() { - anyhow::bail!("null pointer"); - } - let s = CString::from_raw(ptr); - Ok(s.into_string()?) -} diff --git a/backend-rust/crates/module-host/Cargo.toml b/backend-rust/crates/module-host/Cargo.toml deleted file mode 100644 index efe26ed..0000000 --- a/backend-rust/crates/module-host/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "owly-news-module-host" -version.workspace = true -edition.workspace = true - -[lib] -path = "src/lib.rs" - -[dependencies] -anyhow = { workspace = true } -libloading = { workspace = true } -once_cell = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -tokio = { workspace = true, features = ["rt-multi-thread", "macros", "sync"] } -tracing = { workspace = true } -owly-news-module-api = { path = "../module-api" } diff --git a/backend-rust/crates/module-host/src/lib.rs b/backend-rust/crates/module-host/src/lib.rs deleted file mode 100644 index 3d02169..0000000 --- a/backend-rust/crates/module-host/src/lib.rs +++ /dev/null @@ -1,114 +0,0 @@ -use anyhow::Context; -use libloading::{Library, Symbol}; -use once_cell::sync::OnceCell; -use owly_news_module_api::{take_cstring, SYMBOL_INVOKE, SYMBOL_NAME}; -use std::collections::HashMap; -use std::ffi::CString; -use std::os::raw::c_char; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use tokio::sync::Mutex; -use tracing::info; - -type ModuleNameFn = unsafe extern "C" fn() -> *const c_char; -type ModuleInvokeFn = unsafe extern "C" fn(*const c_char, *const c_char) -> *mut c_char; - -pub struct ModuleHandle { - _lib: Arc, - invoke: ModuleInvokeFn, -} - -impl ModuleHandle { - pub fn invoke_json(&self, op: &str, payload: serde_json::Value) -> anyhow::Result { - let op_c = CString::new(op)?; - let payload_c = CString::new(serde_json::to_string(&payload)?)?; - - let out_ptr = unsafe { (self.invoke)(op_c.as_ptr(), payload_c.as_ptr()) }; - let out = unsafe { take_cstring(out_ptr) }?; - let val = serde_json::from_str(&out).context("module returned invalid JSON")?; - Ok(val) - } -} - -pub struct ModuleHost { - // Lazy cache of loaded modules by logical name - loaded: Mutex>>, - modules_dir: PathBuf, -} - -static DEFAULT_HOST: OnceCell> = OnceCell::new(); - -impl ModuleHost { - pub fn default() -> Arc { - DEFAULT_HOST - .get_or_init(|| { - Arc::new(Self::new( - std::env::var_os("OWLY_MODULES_DIR") - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from("target/modules")), // default location - )) - }) - .clone() - } - - pub fn new(modules_dir: PathBuf) -> Self { - Self { - loaded: Mutex::new(HashMap::new()), - modules_dir, - } - } - - pub async fn get(&self, name: &str) -> anyhow::Result> { - if let Some(h) = self.loaded.lock().await.get(name).cloned() { - return Ok(h); - } - let handle = Arc::new(self.load_module(name)?); - self.loaded.lock().await.insert(name.to_string(), handle.clone()); - Ok(handle) - } - - fn load_module(&self, name: &str) -> anyhow::Result { - let lib_path = resolve_module_path(&self.modules_dir, name)?; - info!(module = name, path = %lib_path.display(), "loading module"); - - // SAFETY: we keep Library alive in ModuleHandle to ensure symbols remain valid - let lib = unsafe { Library::new(lib_path) }.with_context(|| "failed to load module library")?; - - // Validate and bind symbols - let name_fn: Symbol = unsafe { lib.get(SYMBOL_NAME.as_bytes()) } - .with_context(|| "missing symbol `module_name`")?; - let invoke_fn: Symbol = unsafe { lib.get(SYMBOL_INVOKE.as_bytes()) } - .with_context(|| "missing symbol `module_invoke`")?; - - // Optional: verify reported name matches requested - let c_name_ptr = unsafe { name_fn() }; - let c_name = unsafe { std::ffi::CStr::from_ptr(c_name_ptr) }.to_string_lossy().into_owned(); - if c_name != name { - anyhow::bail!("module reported name `{c_name}`, expected `{name}`"); - } - - // Copy the function pointer before moving the library - let invoke_fn_copy = *invoke_fn; - - Ok(ModuleHandle { - _lib: Arc::new(lib), - invoke: invoke_fn_copy, - }) - } -} - -fn resolve_module_path(dir: &Path, name: &str) -> anyhow::Result { - #[cfg(target_os = "windows")] - const EXT: &str = "dll"; - #[cfg(target_os = "macos")] - const EXT: &str = "dylib"; - #[cfg(all(unix, not(target_os = "macos")))] - const EXT: &str = "so"; - - let fname = format!("lib{name}.{EXT}"); - let path = dir.join(fname); - if !path.exists() { - anyhow::bail!("module `{name}` not found at {}", path.display()); - } - Ok(path) -} diff --git a/backend-rust/crates/modules/summarizer/Cargo.toml b/backend-rust/crates/modules/summarizer/Cargo.toml deleted file mode 100644 index 84859e8..0000000 --- a/backend-rust/crates/modules/summarizer/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "owly-news-module-summarizer" -version.workspace = true -edition.workspace = true - -[lib] -crate-type = ["cdylib"] -path = "src/lib.rs" - -[dependencies] -anyhow = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -owly-news-module-api = { path = "../../module-api" } diff --git a/backend-rust/crates/modules/summarizer/src/lib.rs b/backend-rust/crates/modules/summarizer/src/lib.rs deleted file mode 100644 index 71d57b4..0000000 --- a/backend-rust/crates/modules/summarizer/src/lib.rs +++ /dev/null @@ -1,50 +0,0 @@ -use owly_news_module_api::{cstr_to_str, string_to_cstring_ptr}; -use serde::{Deserialize, Serialize}; -use std::os::raw::c_char; - -#[derive(Deserialize)] -struct SummarizeReq { - text: String, - #[serde(default = "default_ratio")] - ratio: f32, -} - -fn default_ratio() -> f32 { 0.2 } - -#[derive(Serialize)] -struct SummarizeResp { - summary: String, -} - -#[unsafe(no_mangle)] -pub extern "C" fn module_name() -> *const c_char { - // IMPORTANT: string must live forever; use a const C string - static NAME: &str = "summarizer\0"; - NAME.as_ptr() as *const c_char -} - -#[unsafe(no_mangle)] -pub extern "C" fn module_invoke(op: *const c_char, payload: *const c_char) -> *mut c_char { - // SAFETY: called by trusted host with valid pointers - let res = (|| -> anyhow::Result { - let op = unsafe { cstr_to_str(op)? }; - let payload = unsafe { cstr_to_str(payload)? }; - - match op { - "summarize" => { - let req: SummarizeReq = serde_json::from_str(payload)?; - // Placeholder summarization logic. Replace with real algorithm. - let words: Vec<&str> = req.text.split_whitespace().collect(); - let take = ((words.len() as f32) * req.ratio).max(1.0).round() as usize; - let summary = words.into_iter().take(take).collect::>().join(" "); - let resp = SummarizeResp { summary }; - Ok(serde_json::to_string(&resp)?) - } - _ => anyhow::bail!("unknown op: {op}"), - } - })(); - - let json = res.unwrap_or_else(|e| serde_json::json!({ "error": e.to_string() }).to_string()); - - string_to_cstring_ptr(json) -} diff --git a/backend-rust/crates/server/Cargo.toml b/backend-rust/crates/server/Cargo.toml index 4ef5f47..5218e20 100644 --- a/backend-rust/crates/server/Cargo.toml +++ b/backend-rust/crates/server/Cargo.toml @@ -11,11 +11,12 @@ tracing-subscriber = { workspace = true } axum = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -sqlx = { workspace = true } +sqlx = { workspace = true, features = ["sqlite"] } dotenv = { workspace = true } once_cell = { workspace = true } api = { path = "../api" } +db = { path = "../db" } http = "1.3.1" [features] diff --git a/backend-rust/crates/server/src/lib.rs b/backend-rust/crates/server/src/lib.rs index 9774a87..e889622 100644 --- a/backend-rust/crates/server/src/lib.rs +++ b/backend-rust/crates/server/src/lib.rs @@ -6,6 +6,7 @@ use tracing_subscriber::EnvFilter; use api::services::{DefaultHealthService, HealthService}; use api::types::Health; +use api::config::AppSettings; pub struct AppState { pub health_service: Arc, @@ -29,14 +30,18 @@ async fn health_handler(state: Arc) -> Json { pub async fn start_server(addr: SocketAddr) -> anyhow::Result<()> { init_tracing(); - // TODO: initialize database pools and other infrastructure here. - // let pool = sqlx::PgPool::connect(&db_url).await?; + // Load application settings and initialize the database pool (sqlite). + let app_settings = AppSettings::get_app_settings(); + let pool = db::initialize_db(&app_settings).await?; let state = Arc::new(AppState { health_service: Arc::new(DefaultHealthService), }); - let app = build_router(state).await; + // Base daemon router + let app = build_router(state).await + // Attach API under /api and provide DB state + .nest("/api", api::api::routes::routes().with_state(pool.clone())); let listener = TcpListener::bind(addr).await?; info!("HTTP server listening on http://{}", addr); diff --git a/backend-rust/src/config.rs b/backend-rust/src/config.rs deleted file mode 100644 index 2684b30..0000000 --- a/backend-rust/src/config.rs +++ /dev/null @@ -1,1003 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::env; -use std::path::PathBuf; - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(default)] -pub struct ConfigFile { - #[serde(default)] - pub server: Server, - #[serde(default)] - pub display: ConfDisplay, - #[serde(default)] - pub analytics: Analytics, - #[serde(default)] - pub filtering: Filtering, - #[serde(default)] - pub sharing: Sharing, - #[serde(default)] - pub ai: Ai, - #[serde(default)] - pub scraping: Scraping, - #[serde(default)] - pub processing: Processing, - #[serde(default)] - pub migration: ConfMigration, - #[serde(default)] - pub cli: Cli, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(default)] -pub struct ContentQuality { - pub min_content_length: u32, - pub max_content_length: u32, - pub min_text_html_ratio: f32, - pub readability_threshold: f32, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(default)] -pub struct DuplicateDetection { - pub enabled: bool, - pub title_similarity_threshold: f32, - pub content_similarity_threshold: f32, - pub check_historical_days: u32, - pub store_fingerprints: bool, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(default)] -pub struct AdPrevention { - pub enabled: bool, - pub block_iframes: bool, - pub clean_content: bool, - pub ad_patterns: Vec, - pub preserved_elements: Vec, - pub removed_elements: Vec, -} - -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] -pub enum DefaultView { - Compact, - Full, - Summary, -} - -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] -pub enum Theme { - Light, - Dark, - Auto, -} - -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] -pub enum DefaultSort { - #[serde(rename = "added_desc")] - AddedDesc, - #[serde(rename = "published_desc")] - PublishedDesc, - #[serde(rename = "title_asc")] - TitleAsc, -} - -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] -pub enum DefaultFormat { - #[serde(rename = "text")] - Text, - #[serde(rename = "markdown")] - Markdown, - #[serde(rename = "json")] - JSON, - #[serde(rename = "html")] - HTML, -} - -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] -pub enum AiProvider { - #[serde(rename = "ollama")] - Ollama, - #[serde(rename = "openai")] - OpenAi, - #[serde(rename = "anthropic")] - Anthropic, -} - -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] -pub enum DefaultOutput { - #[serde(rename = "table")] - Table, - #[serde(rename = "json")] - JSON, - #[serde(rename = "csv")] - CSV, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(default)] -pub struct Server { - #[serde(default = "Server::default_host")] - pub host: String, - #[serde(default = "Server::default_port")] - pub port: u16, -} -impl Server { - fn default_host() -> String { - Server::default().host - } - fn default_port() -> u16 { - Server::default().port - } -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(default)] -pub struct ConfDisplay { - #[serde(default)] - pub default_view: DefaultView, - #[serde(default = "ConfDisplay::default_articles_per_page")] - pub articles_per_page: u32, - #[serde(default = "ConfDisplay::default_show_reading_time")] - pub show_reading_time: bool, - #[serde(default = "ConfDisplay::default_show_word_count")] - pub show_word_count: bool, - #[serde(default = "ConfDisplay::default_highlight_unread")] - pub highlight_unread: bool, - #[serde(default)] - pub theme: Theme, -} -impl ConfDisplay { - fn default_articles_per_page() -> u32 { - ConfDisplay::default().articles_per_page - } - fn default_show_reading_time() -> bool { - ConfDisplay::default().show_reading_time - } - fn default_show_word_count() -> bool { - ConfDisplay::default().show_word_count - } - fn default_highlight_unread() -> bool { - ConfDisplay::default().highlight_unread - } -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(default)] -pub struct Analytics { - #[serde(default = "Analytics::default_enabled")] - pub enabled: bool, - #[serde(default = "Analytics::default_track_reading_time")] - pub track_reading_time: bool, - #[serde(default = "Analytics::default_track_scroll_position")] - pub track_scroll_position: bool, - #[serde(default = "Analytics::default_retention_days")] - pub retention_days: u32, - #[serde(default = "Analytics::default_aggregate_older_data")] - pub aggregate_older_data: bool, -} -impl Analytics { - fn default_enabled() -> bool { - Analytics::default().enabled - } - fn default_track_reading_time() -> bool { - Analytics::default().track_reading_time - } - fn default_track_scroll_position() -> bool { - Analytics::default().track_scroll_position - } - fn default_retention_days() -> u32 { - Analytics::default().retention_days - } - fn default_aggregate_older_data() -> bool { - Analytics::default().aggregate_older_data - } -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(default)] -pub struct Filtering { - #[serde(default = "Filtering::default_enable_smart_suggestions")] - pub enable_smart_suggestions: bool, - #[serde(default = "Filtering::default_max_recent_filters")] - pub max_recent_filters: u32, - #[serde(default = "Filtering::default_auto_save_filters")] - pub auto_save_filters: bool, - #[serde(default)] - pub default_sort: DefaultSort, - #[serde(default = "Filtering::default_enable_geographic_hierarchy")] - pub enable_geographic_hierarchy: bool, - #[serde(default = "Filtering::default_auto_migrate_country_filters")] - pub auto_migrate_country_filters: bool, -} -impl Filtering { - fn default_enable_smart_suggestions() -> bool { - Filtering::default().enable_smart_suggestions - } - fn default_max_recent_filters() -> u32 { - Filtering::default().max_recent_filters - } - fn default_auto_save_filters() -> bool { - Filtering::default().auto_save_filters - } - fn default_enable_geographic_hierarchy() -> bool { - Filtering::default().enable_geographic_hierarchy - } - fn default_auto_migrate_country_filters() -> bool { - Filtering::default().auto_migrate_country_filters - } -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(default)] -pub struct Sharing { - #[serde(default)] - pub default_format: DefaultFormat, - #[serde(default = "Sharing::default_include_summary")] - pub include_summary: bool, - #[serde(default = "Sharing::default_include_tags")] - pub include_tags: bool, - #[serde(default = "Sharing::default_include_source")] - pub include_source: bool, - #[serde(default = "Sharing::default_copy_to_clipboard")] - pub copy_to_clipboard: bool, - #[serde(default)] - pub templates: SharingTemplates, -} -impl Sharing { - fn default_include_summary() -> bool { - Sharing::default().include_summary - } - fn default_include_tags() -> bool { - Sharing::default().include_tags - } - fn default_include_source() -> bool { - Sharing::default().include_source - } - fn default_copy_to_clipboard() -> bool { - Sharing::default().copy_to_clipboard - } -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(default)] -pub struct SharingTemplates { - #[serde(default = "SharingTemplate::default_text")] - pub text: SharingTemplate, - #[serde(default = "SharingTemplate::default_markdown")] - pub markdown: SharingTemplate, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(default)] -pub struct SharingTemplate { - pub format: String, -} -impl Default for SharingTemplate { - fn default() -> Self { - // Fallback empty only if used generically; specific fields use the richer defaults below. - Self { - format: String::new(), - } - } -} -impl SharingTemplate { - pub fn default_text() -> Self { - SharingTemplates::default().text - } - pub fn default_markdown() -> Self { - SharingTemplates::default().markdown - } -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(default)] -pub struct Ai { - #[serde(default = "Ai::default_enabled")] - pub enabled: bool, - #[serde(default)] - pub provider: AiProvider, - #[serde(default = "Ai::default_timeout_seconds")] - pub timeout_seconds: u32, - #[serde(default)] - pub summary: AiSummary, - #[serde(default)] - pub tagging: AiTagging, -} -impl Ai { - fn default_enabled() -> bool { - Ai::default().enabled - } - fn default_timeout_seconds() -> u32 { - Ai::default().timeout_seconds - } -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(default)] -pub struct AiSummary { - #[serde(default = "AiSummary::default_enabled")] - pub enabled: bool, - #[serde(default = "AiSummary::default_temperature")] - pub temperature: f32, - #[serde(default = "AiSummary::default_max_tokens")] - pub max_tokens: u32, -} -impl AiSummary { - fn default_enabled() -> bool { - AiSummary::default().enabled - } - fn default_temperature() -> f32 { - AiSummary::default().temperature - } - fn default_max_tokens() -> u32 { - AiSummary::default().max_tokens - } -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(default)] -pub struct AiTagging { - #[serde(default = "AiTagging::default_enabled")] - pub enabled: bool, - #[serde(default = "AiTagging::default_temperature")] - pub temperature: f32, - #[serde(default = "AiTagging::default_max_tokens")] - pub max_tokens: u32, - #[serde(default = "AiTagging::default_max_tags_per_article")] - pub max_tags_per_article: u32, - #[serde(default = "AiTagging::default_min_confidence_threshold")] - pub min_confidence_threshold: f32, - #[serde(default = "AiTagging::default_enable_geographic_tagging")] - pub enable_geographic_tagging: bool, - #[serde(default = "AiTagging::default_enable_category_tagging")] - pub enable_category_tagging: bool, - #[serde(default = "AiTagging::default_geographic_hierarchy_levels")] - pub geographic_hierarchy_levels: u32, -} -impl AiTagging { - fn default_enabled() -> bool { - AiTagging::default().enabled - } - fn default_temperature() -> f32 { - AiTagging::default().temperature - } - fn default_max_tokens() -> u32 { - AiTagging::default().max_tokens - } - fn default_max_tags_per_article() -> u32 { - AiTagging::default().max_tags_per_article - } - fn default_min_confidence_threshold() -> f32 { - AiTagging::default().min_confidence_threshold - } - fn default_enable_geographic_tagging() -> bool { - AiTagging::default().enable_geographic_tagging - } - fn default_enable_category_tagging() -> bool { - AiTagging::default().enable_category_tagging - } - fn default_geographic_hierarchy_levels() -> u32 { - AiTagging::default().geographic_hierarchy_levels - } -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(default)] -pub struct Scraping { - #[serde(default = "Scraping::default_timeout_seconds")] - pub timeout_seconds: u32, - #[serde(default = "Scraping::default_max_retries")] - pub max_retries: u32, - #[serde(default = "Scraping::default_max_content_length")] - pub max_content_length: u32, - #[serde(default = "Scraping::default_respect_robots_txt")] - pub respect_robots_txt: bool, - #[serde(default = "Scraping::default_rate_limit_delay_ms")] - pub rate_limit_delay_ms: u32, - #[serde(default)] - pub content_quality: ContentQuality, - #[serde(default)] - pub duplicate_detection: DuplicateDetection, - #[serde(default)] - pub ad_prevention: AdPrevention, -} -impl Scraping { - fn default_timeout_seconds() -> u32 { - Scraping::default().timeout_seconds - } - fn default_max_retries() -> u32 { - Scraping::default().max_retries - } - fn default_max_content_length() -> u32 { - Scraping::default().max_content_length - } - fn default_respect_robots_txt() -> bool { - Scraping::default().respect_robots_txt - } - fn default_rate_limit_delay_ms() -> u32 { - Scraping::default().rate_limit_delay_ms - } -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(default)] -pub struct Processing { - #[serde(default = "Processing::default_batch_size")] - pub batch_size: u32, - #[serde(default = "Processing::default_max_concurrent")] - pub max_concurrent: u32, - #[serde(default = "Processing::default_retry_attempts")] - pub retry_attempts: u32, - #[serde(default = "Processing::default_priority_manual")] - pub priority_manual: bool, - #[serde(default = "Processing::default_auto_mark_read_on_view")] - pub auto_mark_read_on_view: bool, -} -impl Processing { - fn default_batch_size() -> u32 { - Processing::default().batch_size - } - fn default_max_concurrent() -> u32 { - Processing::default().max_concurrent - } - fn default_retry_attempts() -> u32 { - Processing::default().retry_attempts - } - fn default_priority_manual() -> bool { - Processing::default().priority_manual - } - fn default_auto_mark_read_on_view() -> bool { - Processing::default().auto_mark_read_on_view - } -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(default)] -pub struct ConfMigration { - #[serde(default = "ConfMigration::default_auto_convert_country_filters")] - pub auto_convert_country_filters: bool, - #[serde(default = "ConfMigration::default_preserve_legacy_data")] - pub preserve_legacy_data: bool, - #[serde(default = "ConfMigration::default_migration_batch_size")] - pub migration_batch_size: u32, -} -impl ConfMigration { - fn default_auto_convert_country_filters() -> bool { - ConfMigration::default().auto_convert_country_filters - } - fn default_preserve_legacy_data() -> bool { - ConfMigration::default().preserve_legacy_data - } - fn default_migration_batch_size() -> u32 { - ConfMigration::default().migration_batch_size - } -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(default)] -pub struct Cli { - #[serde(default)] - pub default_output: DefaultOutput, - #[serde(default = "Cli::default_pager_command")] - pub pager_command: String, - #[serde(default = "Cli::default_show_progress")] - pub show_progress: bool, - #[serde(default = "Cli::default_auto_confirm_bulk")] - pub auto_confirm_bulk: bool, - #[serde(default = "Cli::default_show_geographic_hierarchy")] - pub show_geographic_hierarchy: bool, -} -impl Cli { - fn default_pager_command() -> String { - Cli::default().pager_command - } - fn default_show_progress() -> bool { - Cli::default().show_progress - } - fn default_auto_confirm_bulk() -> bool { - Cli::default().auto_confirm_bulk - } - fn default_show_geographic_hierarchy() -> bool { - Cli::default().show_geographic_hierarchy - } -} - -impl Default for ContentQuality { - fn default() -> Self { - Self { - min_content_length: 100, - max_content_length: 500_000, - min_text_html_ratio: 0.3, - readability_threshold: 0.6, - } - } -} - -impl Default for DuplicateDetection { - fn default() -> Self { - Self { - enabled: true, - title_similarity_threshold: 0.90, - content_similarity_threshold: 0.85, - check_historical_days: 30, - store_fingerprints: true, - } - } -} - -impl Default for AdPrevention { - fn default() -> Self { - Self { - enabled: true, - block_iframes: true, - clean_content: true, - ad_patterns: vec![ - "sponsored-content".to_string(), - "advertisement".to_string(), - "promoted-post".to_string(), - "partner-content".to_string(), - "ad-wrapper".to_string(), - "sponsored".to_string(), - ], - preserved_elements: vec![ - "article".to_string(), - "p".to_string(), - "h1".to_string(), - "h2".to_string(), - "h3".to_string(), - "img".to_string(), - "figure".to_string(), - "figcaption".to_string(), - ], - removed_elements: vec![ - "aside".to_string(), - "iframe".to_string(), - "script".to_string(), - "style".to_string(), - "ins".to_string(), - "form".to_string(), - "button".to_string(), - ".ad".to_string(), - "#ad".to_string(), - "[class*='ad-']".to_string(), - "[id*='ad-']".to_string(), - ], - } - } -} - -impl DefaultView { - pub fn display_name(&self) -> &str { - match self { - DefaultView::Compact => "Compact", - DefaultView::Full => "Full Article", - DefaultView::Summary => "Summary", - } - } - - pub fn from_string(s: &str) -> Option { - match s.to_lowercase().as_str() { - "compact" => Some(DefaultView::Compact), - "full" => Some(DefaultView::Full), - "summary" => Some(DefaultView::Summary), - _ => None, - } - } -} - -impl Default for DefaultView { - fn default() -> Self { - DefaultView::Compact - } -} - -impl Theme { - pub fn display_name(&self) -> &str { - match self { - Theme::Light => "Light Mode", - Theme::Dark => "Dark Mode", - Theme::Auto => "Auto (System)", - } - } - - pub fn is_dark_mode(&self, system_prefers_dark: bool) -> bool { - match self { - Theme::Light => false, - Theme::Dark => true, - Theme::Auto => system_prefers_dark, - } - } -} - -impl Default for Theme { - fn default() -> Self { - Theme::Auto - } -} - -impl DefaultSort { - pub fn display_name(&self) -> &str { - match self { - DefaultSort::AddedDesc => "Recently Added", - DefaultSort::PublishedDesc => "Recently Published", - DefaultSort::TitleAsc => "Title A-Z", - } - } - - pub fn to_sql_order(&self) -> &str { - match self { - DefaultSort::AddedDesc => "added_at DESC", - DefaultSort::PublishedDesc => "published_at DESC", - DefaultSort::TitleAsc => "title ASC", - } - } -} - -impl Default for DefaultSort { - fn default() -> Self { - DefaultSort::AddedDesc - } -} - -impl DefaultFormat { - pub fn display_name(&self) -> &str { - match self { - DefaultFormat::Text => "Plain Text", - DefaultFormat::Markdown => "Markdown", - DefaultFormat::JSON => "JSON", - DefaultFormat::HTML => "HTML", - } - } - - pub fn mime_type(&self) -> &str { - match self { - DefaultFormat::Text => "text/plain", - DefaultFormat::Markdown => "text/markdown", - DefaultFormat::JSON => "application/json", - DefaultFormat::HTML => "text/html", - } - } -} - -impl Default for DefaultFormat { - fn default() -> Self { - DefaultFormat::Text - } -} - -impl AiProvider { - pub fn display_name(&self) -> &str { - match self { - AiProvider::Ollama => "Ollama", - AiProvider::OpenAi => "OpenAI", - AiProvider::Anthropic => "Anthropic", - } - } -} - -impl Default for AiProvider { - fn default() -> Self { - AiProvider::Ollama - } -} - -impl DefaultOutput { - pub fn display_name(&self) -> &str { - match self { - DefaultOutput::Table => "Table", - DefaultOutput::JSON => "JSON", - DefaultOutput::CSV => "CSV", - } - } -} - -impl Default for DefaultOutput { - fn default() -> Self { - DefaultOutput::Table - } -} - -impl Default for Cli { - fn default() -> Self { - Self { - default_output: DefaultOutput::Table, - pager_command: "less -R".to_string(), - show_progress: true, - auto_confirm_bulk: false, - show_geographic_hierarchy: true, - } - } -} - -impl Default for ConfMigration { - fn default() -> Self { - Self { - auto_convert_country_filters: true, - preserve_legacy_data: true, - migration_batch_size: 100, - } - } -} - -impl Default for Processing { - fn default() -> Self { - Self { - batch_size: 10, - max_concurrent: 5, - retry_attempts: 3, - priority_manual: true, - auto_mark_read_on_view: false, - } - } -} - -impl Default for Scraping { - fn default() -> Self { - Self { - timeout_seconds: 30, - max_retries: 3, - max_content_length: 100_000, - respect_robots_txt: true, - rate_limit_delay_ms: 1000, - content_quality: ContentQuality::default(), - duplicate_detection: DuplicateDetection::default(), - ad_prevention: AdPrevention::default(), - } - } -} - -impl Default for Ai { - fn default() -> Self { - Self { - enabled: true, - provider: AiProvider::default(), - timeout_seconds: 120, - summary: AiSummary::default(), - tagging: AiTagging::default(), - } - } -} - -impl Default for AiSummary { - fn default() -> Self { - Self { - enabled: true, - temperature: 0.1, - max_tokens: 1024, - } - } -} - -impl Default for AiTagging { - fn default() -> Self { - Self { - enabled: true, - temperature: 0.3, - max_tokens: 200, - max_tags_per_article: 10, - min_confidence_threshold: 0.7, - enable_geographic_tagging: true, - enable_category_tagging: true, - geographic_hierarchy_levels: 3, - } - } -} - -impl Default for Sharing { - fn default() -> Self { - Self { - default_format: DefaultFormat::default(), - include_summary: true, - include_tags: true, - include_source: true, - copy_to_clipboard: true, - templates: SharingTemplates::default(), - } - } -} - -impl Default for SharingTemplates { - fn default() -> Self { - Self { - text: SharingTemplate { - format: r#"📰 {title} - -{summary} - -🏷️ Tags: {tags} -🌍 Location: {geographic_tags} -🔗 Source: {url} -📅 Published: {published_at} - -Shared via Owly News Summariser -"# - .to_string(), - }, - markdown: SharingTemplate { - format: r#"# {title} - -{summary} - -**Tags:** {tags} -**Location:** {geographic_tags} -**Source:** [{url}]({url}) -**Published:** {published_at} - ---- -*Shared via Owly News Summariser* -"# - .to_string(), - }, - } - } -} - -impl Default for Filtering { - fn default() -> Self { - Self { - enable_smart_suggestions: true, - max_recent_filters: 10, - auto_save_filters: true, - default_sort: DefaultSort::AddedDesc, - enable_geographic_hierarchy: true, - auto_migrate_country_filters: true, - } - } -} - -impl Default for Analytics { - fn default() -> Self { - Self { - enabled: true, - track_reading_time: true, - track_scroll_position: true, - retention_days: 365, - aggregate_older_data: true, - } - } -} - -impl Default for ConfDisplay { - fn default() -> Self { - Self { - default_view: DefaultView::default(), - articles_per_page: 50, - show_reading_time: true, - show_word_count: false, - highlight_unread: true, - theme: Theme::default(), - } - } -} - -impl Default for Server { - fn default() -> Self { - Self { - host: "127.0.0.1".to_string(), - port: 8090, - } - } -} -impl Default for ConfigFile { - fn default() -> Self { - Self { - server: Server::default(), - display: ConfDisplay::default(), - analytics: Analytics::default(), - filtering: Filtering::default(), - sharing: Sharing::default(), - ai: Ai::default(), - scraping: Scraping::default(), - processing: Processing::default(), - migration: ConfMigration::default(), - cli: Cli::default(), - } - } -} - -impl ConfigFile { - pub fn load_from_file(app_settings: &AppSettings) -> Result> { - let config_path_str = &app_settings.config_path; - let config_path = PathBuf::from(config_path_str); - - if !config_path.exists() { - let config_file = ConfigFile::default(); - config_file.save_to_file(app_settings)?; - return Ok(config_file); - } - - let contents = std::fs::read_to_string(config_path_str)?; - let config: ConfigFile = toml::from_str(&contents)?; - Ok(config) - } - - pub fn save_to_file( - &self, - app_settings: &AppSettings, - ) -> Result<(), Box> { - let contents = toml::to_string_pretty(self)?; - std::fs::write(&app_settings.config_path, contents)?; - Ok(()) - } -} - -#[derive(Deserialize, Debug)] -pub struct AppSettings { - pub config_path: String, - pub db_path: String, - pub migration_path: String, - pub config: ConfigFile, -} - -impl Default for AppSettings { - fn default() -> Self { - Self { - config_path: Self::get_config_path(), - db_path: Self::get_db_path(), - migration_path: Self::get_migration_path(), - config: ConfigFile::default(), - } - } -} - -impl AppSettings { - const FALLBACK_DIR: &'static str = "/tmp"; - const PROD_DB_PATH: &'static str = "/var/lib/owly-news/owlynews.sqlite3"; - const PROD_MIGRATION_PATH: &'static str = "/usr/share/owly-news/migrations"; - - pub fn get_app_settings() -> Self { - AppSettings::default() - } - - fn get_base_path(env_var: &str, fallback: &str) -> String { - env::var(env_var).unwrap_or_else(|_| fallback.to_string()) - } - - fn get_db_path() -> String { - if cfg!(debug_assertions) { - let manifest_dir = Self::get_base_path("CARGO_MANIFEST_DIR", Self::FALLBACK_DIR); - format!("{}/owlynews.sqlite3", manifest_dir) - } else { - Self::PROD_DB_PATH.to_string() - } - } - - fn get_config_path() -> String { - if cfg!(debug_assertions) { - let manifest_dir = Self::get_base_path("CARGO_MANIFEST_DIR", Self::FALLBACK_DIR); - format!("{}/config.toml", manifest_dir) - } else { - let home = Self::get_base_path("HOME", Self::FALLBACK_DIR); - format!("{}/.config/owly-news/config.toml", home) - } - } - - fn get_migration_path() -> String { - if cfg!(debug_assertions) { - let manifest_dir = Self::get_base_path("CARGO_MANIFEST_DIR", Self::FALLBACK_DIR); - format!("{}/migrations", manifest_dir) - } else { - Self::PROD_MIGRATION_PATH.to_string() - } - } - - pub fn database_url(&self) -> String { - format!("sqlite:{}", self.db_path) - } - - pub fn ensure_default_directory(&self) -> Result<(), std::io::Error> { - // Always create the database directory - if let Some(parent) = std::path::Path::new(&self.db_path).parent() { - std::fs::create_dir_all(parent)?; - } - - // In production mode, also create the config directory - if !cfg!(debug_assertions) { - if let Some(parent) = std::path::Path::new(&self.config_path).parent() { - std::fs::create_dir_all(parent)?; - } - } - - Ok(()) - } -} diff --git a/backend-rust/src/main.rs b/backend-rust/src/main.rs deleted file mode 100644 index 9c7bfaa..0000000 --- a/backend-rust/src/main.rs +++ /dev/null @@ -1,104 +0,0 @@ -mod config; -mod db; -mod models; -mod services; - -use crate::config::{AppSettings, ConfigFile}; -use anyhow::Result; -use axum::Router; -use axum::routing::get; -use tokio::signal; -use tracing::info; -use tracing_subscriber; - -#[tokio::main] -async fn main() -> Result<()> { - init_logging(); - - info!("Starting server"); - - let app_settings = load_app_settings(); - - let pool = db::initialize_db(&app_settings).await?; - let app = create_app(pool); - - let listener = tokio::net::TcpListener::bind(format!( - "{}:{}", - &app_settings.config.server.host, &app_settings.config.server.port - )) - .await?; - info!( - "Server starting on http://{}:{}", - &app_settings.config.server.host, &app_settings.config.server.port - ); - - axum::serve(listener, app) - .with_graceful_shutdown(shutdown_signal()) - .await?; - info!("Server stopped"); - - Ok(()) -} - -fn create_app(pool: sqlx::SqlitePool) -> Router { - Router::new() - .route("/health", get(health_check)) - .nest("/api", api::routes::routes()) - .with_state(pool) -} - -async fn health_check() -> &'static str { - "OK" -} - -fn init_logging() { - tracing_subscriber::fmt() - .with_target(false) - .compact() - // .with_env_filter(EnvFilter::from_default_env()) - // .json() // For Production - .init(); -} - -fn load_app_settings() -> AppSettings { - AppSettings::default(); - let app_settings = AppSettings::get_app_settings(); - - AppSettings::ensure_default_directory(&app_settings) - .expect("Failed to create default directory"); - - let config = ConfigFile::load_from_file(&AppSettings::get_app_settings()) - .expect("Failed to load config file"); - - let app_settings = AppSettings { - config, - ..app_settings - }; - app_settings -} - -async fn shutdown_signal() { - let ctrl_c = async { - signal::ctrl_c() - .await - .expect("failed to install CTRL+C handler"); - }; - - #[cfg(unix)] - let terminate = async { - signal::unix::signal(signal::unix::SignalKind::terminate()) - .expect("failed to install terminate handler") - .recv() - .await; - }; - - #[cfg(not(unix))] - let terminate = std::future::pending::<()>(); - - tokio::select! { - _ = ctrl_c => {}, - _ = terminate => {}, - } - - info!("Signal received, shutting down"); -} diff --git a/backend-rust/src/models.rs b/backend-rust/src/models.rs deleted file mode 100644 index 974e336..0000000 --- a/backend-rust/src/models.rs +++ /dev/null @@ -1,6 +0,0 @@ -mod article; -mod summary; -mod user; -mod tag; -mod analytics; -mod settings; diff --git a/backend-rust/src/models/analytics.rs b/backend-rust/src/models/analytics.rs deleted file mode 100644 index e69de29..0000000 diff --git a/backend-rust/src/models/article.rs b/backend-rust/src/models/article.rs deleted file mode 100644 index e69de29..0000000 diff --git a/backend-rust/src/models/settings.rs b/backend-rust/src/models/settings.rs deleted file mode 100644 index e69de29..0000000 diff --git a/backend-rust/src/models/summary.rs b/backend-rust/src/models/summary.rs deleted file mode 100644 index e69de29..0000000 diff --git a/backend-rust/src/models/tag.rs b/backend-rust/src/models/tag.rs deleted file mode 100644 index e69de29..0000000 diff --git a/backend-rust/src/models/user.rs b/backend-rust/src/models/user.rs deleted file mode 100644 index e69de29..0000000 diff --git a/backend-rust/src/services.rs b/backend-rust/src/services.rs deleted file mode 100644 index d86eac3..0000000 --- a/backend-rust/src/services.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod summary_service; -mod news_service; -mod scraping_service; -mod tagging_service; -mod analytics_service; -mod sharing_service; -pub(crate) mod content_processor; diff --git a/backend-rust/src/services/analytics_service.rs b/backend-rust/src/services/analytics_service.rs deleted file mode 100644 index e69de29..0000000 diff --git a/backend-rust/src/services/news_service.rs b/backend-rust/src/services/news_service.rs deleted file mode 100644 index e69de29..0000000 diff --git a/backend-rust/src/services/scraping_service.rs b/backend-rust/src/services/scraping_service.rs deleted file mode 100644 index e69de29..0000000 diff --git a/backend-rust/src/services/sharing_service.rs b/backend-rust/src/services/sharing_service.rs deleted file mode 100644 index e69de29..0000000 diff --git a/backend-rust/src/services/summary_service.rs b/backend-rust/src/services/summary_service.rs deleted file mode 100644 index e69de29..0000000 diff --git a/backend-rust/src/services/tagging_service.rs b/backend-rust/src/services/tagging_service.rs deleted file mode 100644 index e69de29..0000000