diff --git a/Cargo.lock b/Cargo.lock index a7d22d5..8e8767c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -328,7 +328,7 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" [[package]] name = "fedigroups" -version = "0.3.0" +version = "0.4.0" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 1444b91..33aeffb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fedigroups" -version = "0.3.0" +version = "0.4.0" authors = ["Ondřej Hruška "] edition = "2018" publish = false diff --git a/locales/cs.json b/locales/cs.json new file mode 100644 index 0000000..09393fe --- /dev/null +++ b/locales/cs.json @@ -0,0 +1,4 @@ +{ + "welcome_public": "Ahoj", + "ping_response": "pong, toto je fedigroups verze {version}" +} diff --git a/locales/en.json b/locales/en.json index f04253e..5ed832e 100644 --- a/locales/en.json +++ b/locales/en.json @@ -49,7 +49,6 @@ "cmd_unadmin_ok": "User {user} is no longer a group admin!", "cmd_unadmin_fail_already": "No action, user {user} is not a group admin", - "mention_prefix": "@{user} ", "group_announcement": "**📢Group announcement**\n{message}", "ping_response": "pong, this is fedigroups service v{version}" diff --git a/src/group_handler/handle_mention.rs b/src/group_handler/handle_mention.rs index e5edfdc..d608345 100644 --- a/src/group_handler/handle_mention.rs +++ b/src/group_handler/handle_mention.rs @@ -12,7 +12,7 @@ use crate::error::GroupError; use crate::group_handler::GroupHandle; use crate::store::group_config::GroupConfig; use crate::store::CommonConfig; -use crate::tr::{EMPTY_TRANSLATION_TABLE, TranslationTable}; +use crate::tr::TranslationTable; use crate::utils; use crate::utils::{normalize_acct, LogError}; @@ -35,8 +35,7 @@ pub struct ProcessMention<'a> { impl<'a> ProcessMention<'a> { fn tr(&self) -> &TranslationTable { - self.cc.tr.get(self.config.get_locale()) - .unwrap_or(&EMPTY_TRANSLATION_TABLE) + self.cc.tr(self.config.get_locale()) } async fn lookup_acct_id(&self, acct: &str, followed: bool) -> Result, GroupError> { diff --git a/src/group_handler/mod.rs b/src/group_handler/mod.rs index f9ea4e3..01224a8 100644 --- a/src/group_handler/mod.rs +++ b/src/group_handler/mod.rs @@ -1,5 +1,4 @@ use std::collections::VecDeque; -use std::ops::Deref; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -20,7 +19,7 @@ use crate::command::StatusCommand; use crate::error::GroupError; use crate::store::CommonConfig; use crate::store::GroupConfig; -use crate::tr::{EMPTY_TRANSLATION_TABLE, TranslationTable}; +use crate::tr::TranslationTable; use crate::utils::{normalize_acct, LogError, VisExt}; mod handle_mention; @@ -162,27 +161,31 @@ impl GroupHandle { let socket_open_time = Instant::now(); let mut last_rx = Instant::now(); - match self.catch_up_with_missed_notifications().await { - Ok(true) => { - grp_debug!(self, "Some missed notifs handled"); - } - Ok(false) => { - grp_debug!(self, "No notifs missed"); - } - Err(e) => { - grp_error!(self, "Failed to handle missed notifs: {}", e); + if self.cc.max_catchup_notifs > 0 { + match self.catch_up_with_missed_notifications().await { + Ok(true) => { + grp_debug!(self, "Some missed notifs handled"); + } + Ok(false) => { + grp_debug!(self, "No notifs missed"); + } + Err(e) => { + grp_error!(self, "Failed to handle missed notifs: {}", e); + } } } - match self.catch_up_with_missed_statuses().await { - Ok(true) => { - grp_debug!(self, "Some missed statuses handled"); - } - Ok(false) => { - grp_debug!(self, "No statuses missed"); - } - Err(e) => { - grp_error!(self, "Failed to handle missed statuses: {}", e); + if self.cc.max_catchup_statuses > 0 { + match self.catch_up_with_missed_statuses().await { + Ok(true) => { + grp_debug!(self, "Some missed statuses handled"); + } + Ok(false) => { + grp_debug!(self, "No statuses missed"); + } + Err(e) => { + grp_error!(self, "Failed to handle missed statuses: {}", e); + } } } @@ -526,8 +529,7 @@ impl GroupHandle { } fn tr(&self) -> &TranslationTable { - self.cc.tr.get(self.config.get_locale()) - .unwrap_or(&EMPTY_TRANSLATION_TABLE) + self.cc.tr(self.config.get_locale()) } async fn handle_new_follow(&mut self, notif_acct: &str, notif_user_id: &str) { diff --git a/src/main.rs b/src/main.rs index c3f9541..f139a12 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,12 +10,10 @@ extern crate serde; #[macro_use] extern crate thiserror; -use std::sync::Arc; use clap::Arg; use log::LevelFilter; use crate::store::{NewGroupOptions, StoreOptions}; -use crate::tr::TranslationTable; use crate::utils::acct_to_server; mod command; @@ -132,10 +130,8 @@ async fn main() -> anyhow::Result<()> { store.find_locales().await; - return Ok(()); - // Start - let groups = Arc::new(store).spawn_groups().await?; + let groups = store.spawn_groups().await?; let mut handles = vec![]; for mut g in groups { diff --git a/src/store/common_config.rs b/src/store/common_config.rs index 26f28a7..8732922 100644 --- a/src/store/common_config.rs +++ b/src/store/common_config.rs @@ -1,9 +1,12 @@ use std::collections::HashMap; +use crate::store::DEFAULT_LOCALE_NAME; use crate::tr::TranslationTable; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default, deny_unknown_fields)] pub struct CommonConfig { + pub groups_dir: String, + pub locales_dir: String, /// Max number of missed notifs to process after connect pub max_catchup_notifs: usize, /// Max number of missed statuses to process after connect @@ -31,6 +34,8 @@ pub struct CommonConfig { impl Default for CommonConfig { fn default() -> Self { Self { + groups_dir: "groups".to_string(), + locales_dir: "locales".to_string(), max_catchup_notifs: 30, max_catchup_statuses: 50, delay_fetch_page_s: 0.25, @@ -43,3 +48,12 @@ impl Default for CommonConfig { } } } + +impl CommonConfig { + pub fn tr(&self, lang : &str) -> &TranslationTable { + match self.tr.get(lang) { + Some(tr) => tr, + None => self.tr.get(DEFAULT_LOCALE_NAME).expect("default locale is not loaded") + } + } +} diff --git a/src/store/group_config.rs b/src/store/group_config.rs index ec769f7..7846bbe 100644 --- a/src/store/group_config.rs +++ b/src/store/group_config.rs @@ -4,6 +4,7 @@ use std::path::{Path, PathBuf}; use elefren::AppData; use crate::error::GroupError; +use crate::store::DEFAULT_LOCALE_NAME; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default, deny_unknown_fields)] @@ -75,7 +76,7 @@ impl Default for FixedConfig { Self { enabled: true, acct: "".to_string(), - locale: "en".to_string(), + locale: DEFAULT_LOCALE_NAME.to_string(), appdata: AppData { base: Default::default(), client_id: Default::default(), diff --git a/src/store/mod.rs b/src/store/mod.rs index 80062a2..f087b9f 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -32,6 +32,9 @@ pub struct StoreOptions { pub store_dir: String, } +const DEFAULT_LOCALE_NAME : &str = "en"; +const DEFAULT_LOCALE_JSON : &str = include_str!("../../locales/en.json"); + impl ConfigStore { /// Create a new instance of the store. /// If a path is given, it will try to load the content from a file. @@ -76,13 +79,30 @@ impl ConfigStore { debug!("Using common config:\n{:#?}", config); - let groups_path = base_dir.join("groups"); + let groups_path = if config.groups_dir.starts_with('/') { + PathBuf::from(&config.groups_dir) + } else { + base_dir.join(&config.groups_dir) + }; + if !groups_path.exists() { debug!("Creating groups directory"); tokio::fs::create_dir_all(&groups_path).await?; } - let locales_path = base_dir.join("locales"); + let locales_path = if config.locales_dir.starts_with('/') { + PathBuf::from(&config.locales_dir) + } else { + base_dir.join(&config.locales_dir) + }; + + // warn, this is usually not a good idea beside for testing + if config.max_catchup_notifs == 0 { + warn!("Missed notifications catch-up is disabled!"); + } + if config.max_catchup_statuses == 0 { + warn!("Missed statuses catch-up is disabled!"); + } Ok(Self { store_path: base_dir.to_owned(), @@ -107,7 +127,7 @@ impl ConfigStore { let group_dir = self.groups_path.join(&opts.acct); - let data = GroupConfig::from_appdata(opts.acct.clone(), appdata, group_dir).await?; + let _data = GroupConfig::from_appdata(opts.acct.clone(), appdata, group_dir).await?; // save & persist @@ -149,7 +169,7 @@ impl ConfigStore { config.set_appdata(appdata); config.save_if_needed(true).await?; - let group_account = match client.verify_credentials().await { + let _group_account = match client.verify_credentials().await { Ok(account) => { info!( "Group account verified: @{}, \"{}\"", @@ -167,6 +187,9 @@ impl ConfigStore { } pub async fn find_locales(&mut self) { + // Load the default locale, it will be used as fallback to fill-in missing keys + self.load_locale(DEFAULT_LOCALE_NAME, DEFAULT_LOCALE_JSON, true); + if !self.locales_path.is_dir() { debug!("No locales path set!"); return; @@ -175,7 +198,7 @@ impl ConfigStore { let entries = match std::fs::read_dir(&self.locales_path) { Ok(ee) => ee, Err(e) => { - warn!("Error listing locales"); + warn!("Error listing locales: {}", e); return; } }; @@ -189,13 +212,8 @@ impl ConfigStore { match tokio::fs::read(&path).await { Ok(f) => { - if let Ok(tr) = serde_json::from_slice::(&f) { - let locale_name = path.file_stem().unwrap_or_default().to_string_lossy().to_string(); - debug!("Loaded locale: {}", locale_name); - self.config.tr.insert(locale_name, tr); - } else { - error!("Failed to parse locale file {}", path.display()); - } + let locale_name = path.file_stem().unwrap_or_default().to_string_lossy(); + self.load_locale(&locale_name, &String::from_utf8_lossy(&f), false); }, Err(e) => { error!("Failed to read locale file {}: {}", path.display(), e); @@ -206,6 +224,31 @@ impl ConfigStore { } } + fn load_locale(&mut self, locale_name: &str, locale_json: &str, is_default: bool) { + if let Ok(mut tr) = serde_json::from_str::(locale_json) { + debug!("Loaded locale: {}", locale_name); + + if !is_default { + let def_tr = self.config.tr.get(DEFAULT_LOCALE_NAME).expect("Default locale not loaded!"); + + for (k, v) in def_tr.entries() { + if !tr.translation_exists(k) { + warn!("locale \"{}\" is missing \"{}\", default: {:?}", + locale_name, + k, + def_tr.get_translation_raw(k).unwrap()); + + tr.add_translation(k, v); + } + } + } + + self.config.tr.insert(locale_name.to_owned(), tr); + } else { + error!("Failed to parse locale {}", locale_name); + } + } + /// Spawn existing group using saved creds pub async fn spawn_groups(self) -> Result, GroupError> { info!("Starting group services for groups in {}", self.groups_path.display()); diff --git a/src/tr.rs b/src/tr.rs index 3313303..b4c6d55 100644 --- a/src/tr.rs +++ b/src/tr.rs @@ -1,48 +1,50 @@ //! magic for custom translations and strings use std::collections::HashMap; -use once_cell::sync::Lazy; #[derive(Debug,Clone,Serialize,Deserialize,Default)] pub struct TranslationTable { #[serde(flatten)] - entries: Option>, + entries: HashMap, } -pub const EMPTY_TRANSLATION_TABLE : TranslationTable = TranslationTable { - entries: None, -}; - impl TranslationTable { + #[allow(unused)] pub fn new() -> Self { Self::default() } + /// Iterate all entries + pub fn entries(&self) -> impl Iterator { + self.entries.iter() + } + + pub fn get_translation_raw(&self, key : &str) -> Option<&str> { + self.entries.get(key).map(|s| s.as_str()) + } + pub fn add_translation(&mut self, key : impl ToString, subs : impl ToString) { - if self.entries.is_none() { - self.entries = Some(Default::default()); - } - self.entries.as_mut().unwrap().insert(key.to_string(), subs.to_string()); + self.entries.insert(key.to_string(), subs.to_string()); + } + + pub fn translation_exists(&self, key : &str) -> bool { + self.entries.contains_key(key) } pub fn subs(&self, key : &str, substitutions: &[&str]) -> String { - if let Some(ee) = &self.entries { - match ee.get(key) { - Some(s) => { - // TODO optimize - let mut s = s.clone(); - for pair in substitutions.chunks(2) { - if pair.len() != 2 { - continue; - } - s = s.replace(&format!("{{{}}}", pair[0]), pair[1]); + match self.entries.get(key) { + Some(s) => { + // TODO optimize + let mut s = s.clone(); + for pair in substitutions.chunks(2) { + if pair.len() != 2 { + continue; } - s + s = s.replace(&format!("{{{}}}", pair[0]), pair[1]); } - None => key.to_owned() + s } - } else { - key.to_owned() + None => key.to_owned() } } }