generalbots/botlib/src/branding.rs
Rodrigo Rodriguez (Pragmatismo) 037db5c381 feat: Major workspace reorganization and documentation update
- Add comprehensive documentation in botbook/ with 12 chapters
- Add botapp/ Tauri desktop application
- Add botdevice/ IoT device support
- Add botlib/ shared library crate
- Add botmodels/ Python ML models service
- Add botplugin/ browser extension
- Add botserver/ reorganized server code
- Add bottemplates/ bot templates
- Add bottest/ integration tests
- Add botui/ web UI server
- Add CI/CD workflows in .forgejo/workflows/
- Add AGENTS.md and PROD.md documentation
- Add dependency management scripts (DEPENDENCIES.sh/ps1)
- Remove legacy src/ structure and migrations
- Clean up temporary and backup files
2026-04-19 08:14:25 -03:00

294 lines
8.9 KiB
Rust

use log::info;
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::sync::OnceLock;
static BRANDING: OnceLock<BrandingConfig> = OnceLock::new();
const DEFAULT_PLATFORM_NAME: &str = "General Bots";
const DEFAULT_PLATFORM_SHORT: &str = "GB";
const DEFAULT_PLATFORM_DOMAIN: &str = "generalbots.com";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrandingConfig {
pub name: String,
pub short_name: String,
pub company: Option<String>,
pub domain: Option<String>,
pub support_email: Option<String>,
pub logo_url: Option<String>,
pub favicon_url: Option<String>,
pub primary_color: Option<String>,
pub secondary_color: Option<String>,
pub footer_text: Option<String>,
pub copyright: Option<String>,
pub custom_css: Option<String>,
pub terms_url: Option<String>,
pub privacy_url: Option<String>,
pub docs_url: Option<String>,
pub is_white_label: bool,
}
impl Default for BrandingConfig {
fn default() -> Self {
Self {
name: DEFAULT_PLATFORM_NAME.to_string(),
short_name: DEFAULT_PLATFORM_SHORT.to_string(),
company: Some("pragmatismo.com.br".to_string()),
domain: Some(DEFAULT_PLATFORM_DOMAIN.to_string()),
support_email: Some("support@generalbots.com".to_string()),
logo_url: None,
favicon_url: None,
primary_color: Some("#25d366".to_string()),
secondary_color: Some("#075e54".to_string()),
footer_text: None,
copyright: Some(format!(
"© {} pragmatismo.com.br. All rights reserved.",
chrono::Utc::now().format("%Y")
)),
custom_css: None,
terms_url: None,
privacy_url: None,
docs_url: Some("https://docs.generalbots.com".to_string()),
is_white_label: false,
}
}
}
impl BrandingConfig {
#[must_use]
pub fn load() -> Self {
let search_paths = [
".product",
"config/.product",
"/etc/botserver/.product",
"/opt/gbo/.product",
];
for path in &search_paths {
if let Ok(config) = Self::load_from_file(path) {
info!("Loaded white-label branding from {path}: {}", config.name);
return config;
}
}
if let Ok(product_file) = std::env::var("PRODUCT_FILE") {
if let Ok(config) = Self::load_from_file(&product_file) {
info!(
"Loaded white-label branding from PRODUCT_FILE={product_file}: {}",
config.name
);
return config;
}
}
let mut config = Self::default();
if let Ok(name) = std::env::var("PLATFORM_NAME") {
config.name = name;
config.is_white_label = true;
}
if let Ok(short) = std::env::var("PLATFORM_SHORT_NAME") {
config.short_name = short;
}
if let Ok(company) = std::env::var("PLATFORM_COMPANY") {
config.company = Some(company);
}
if let Ok(domain) = std::env::var("PLATFORM_DOMAIN") {
config.domain = Some(domain);
}
if let Ok(logo) = std::env::var("PLATFORM_LOGO_URL") {
config.logo_url = Some(logo);
}
if let Ok(color) = std::env::var("PLATFORM_PRIMARY_COLOR") {
config.primary_color = Some(color);
}
config
}
fn load_from_file(path: &str) -> Result<Self, Box<dyn std::error::Error>> {
let path = Path::new(path);
if !path.exists() {
return Err("File not found".into());
}
let content = std::fs::read_to_string(path)?;
if let Ok(config) = toml::from_str::<ProductFile>(&content) {
return Ok(config.into());
}
let mut config = Self {
is_white_label: true,
..Self::default()
};
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
continue;
}
if let Some((key, value)) = line.split_once('=') {
let key = key.trim().to_lowercase();
let value = value.trim().trim_matches('"').trim_matches('\'');
match key.as_str() {
"name" | "platform_name" => config.name = value.to_string(),
"short_name" | "short" => config.short_name = value.to_string(),
"company" | "organization" => config.company = Some(value.to_string()),
"domain" => config.domain = Some(value.to_string()),
"support_email" | "email" => config.support_email = Some(value.to_string()),
"logo_url" | "logo" => config.logo_url = Some(value.to_string()),
"favicon_url" | "favicon" => config.favicon_url = Some(value.to_string()),
"primary_color" | "color" => config.primary_color = Some(value.to_string()),
"secondary_color" => config.secondary_color = Some(value.to_string()),
"footer_text" | "footer" => config.footer_text = Some(value.to_string()),
"copyright" => config.copyright = Some(value.to_string()),
"custom_css" | "css" => config.custom_css = Some(value.to_string()),
"terms_url" | "terms" => config.terms_url = Some(value.to_string()),
"privacy_url" | "privacy" => config.privacy_url = Some(value.to_string()),
"docs_url" | "docs" => config.docs_url = Some(value.to_string()),
_ => {}
}
}
}
Ok(config)
}
}
#[derive(Debug, Deserialize)]
struct ProductFile {
name: String,
#[serde(default)]
short_name: Option<String>,
#[serde(default)]
company: Option<String>,
#[serde(default)]
domain: Option<String>,
#[serde(default)]
support_email: Option<String>,
#[serde(default)]
logo_url: Option<String>,
#[serde(default)]
favicon_url: Option<String>,
#[serde(default)]
primary_color: Option<String>,
#[serde(default)]
secondary_color: Option<String>,
#[serde(default)]
footer_text: Option<String>,
#[serde(default)]
copyright: Option<String>,
#[serde(default)]
custom_css: Option<String>,
#[serde(default)]
terms_url: Option<String>,
#[serde(default)]
privacy_url: Option<String>,
#[serde(default)]
docs_url: Option<String>,
}
impl From<ProductFile> for BrandingConfig {
fn from(pf: ProductFile) -> Self {
let short_name = pf.short_name.unwrap_or_else(|| {
pf.name
.split_whitespace()
.map(|w| w.chars().next().unwrap_or('X'))
.collect::<String>()
.to_uppercase()
});
Self {
name: pf.name,
short_name,
company: pf.company,
domain: pf.domain,
support_email: pf.support_email,
logo_url: pf.logo_url,
favicon_url: pf.favicon_url,
primary_color: pf.primary_color,
secondary_color: pf.secondary_color,
footer_text: pf.footer_text,
copyright: pf.copyright,
custom_css: pf.custom_css,
terms_url: pf.terms_url,
privacy_url: pf.privacy_url,
docs_url: pf.docs_url,
is_white_label: true,
}
}
}
pub fn init_branding() {
let config = BrandingConfig::load();
let _ = BRANDING.set(config);
}
#[must_use]
pub fn branding() -> &'static BrandingConfig {
BRANDING.get_or_init(BrandingConfig::load)
}
#[must_use]
pub fn platform_name() -> &'static str {
&branding().name
}
#[must_use]
pub fn platform_short() -> &'static str {
&branding().short_name
}
#[must_use]
pub fn is_white_label() -> bool {
branding().is_white_label
}
#[must_use]
pub fn copyright_text() -> String {
branding().copyright.clone().unwrap_or_else(|| {
format!(
"© {} {}",
chrono::Utc::now().format("%Y"),
branding().company.as_deref().unwrap_or(&branding().name)
)
})
}
#[must_use]
pub fn footer_text() -> String {
branding()
.footer_text
.clone()
.unwrap_or_else(|| format!("Powered by {}", platform_name()))
}
#[must_use]
pub fn log_prefix() -> String {
format!("[{}]", platform_short())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_branding() {
let config = BrandingConfig::default();
assert_eq!(config.name, "General Bots");
assert_eq!(config.short_name, "GB");
assert!(!config.is_white_label);
}
#[test]
fn test_platform_name_function() {
let name = platform_name();
assert!(!name.is_empty());
}
}