fixes for locales, improvements, more logging, stub cs translation

pull/14/head
Ondřej Hruška 3 years ago
parent 239e15afdd
commit e76da157b3
  1. 2
      Cargo.lock
  2. 2
      Cargo.toml
  3. 4
      locales/cs.json
  4. 1
      locales/en.json
  5. 5
      src/group_handler/handle_mention.rs
  6. 46
      src/group_handler/mod.rs
  7. 6
      src/main.rs
  8. 14
      src/store/common_config.rs
  9. 3
      src/store/group_config.rs
  10. 67
      src/store/mod.rs
  11. 50
      src/tr.rs

2
Cargo.lock generated

@ -328,7 +328,7 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
[[package]] [[package]]
name = "fedigroups" name = "fedigroups"
version = "0.3.0" version = "0.4.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",

@ -1,6 +1,6 @@
[package] [package]
name = "fedigroups" name = "fedigroups"
version = "0.3.0" version = "0.4.0"
authors = ["Ondřej Hruška <ondra@ondrovo.com>"] authors = ["Ondřej Hruška <ondra@ondrovo.com>"]
edition = "2018" edition = "2018"
publish = false publish = false

@ -0,0 +1,4 @@
{
"welcome_public": "Ahoj",
"ping_response": "pong, toto je fedigroups verze {version}"
}

@ -49,7 +49,6 @@
"cmd_unadmin_ok": "User {user} is no longer a group admin!", "cmd_unadmin_ok": "User {user} is no longer a group admin!",
"cmd_unadmin_fail_already": "No action, user {user} is not a group admin", "cmd_unadmin_fail_already": "No action, user {user} is not a group admin",
"mention_prefix": "@{user} ", "mention_prefix": "@{user} ",
"group_announcement": "**📢Group announcement**\n{message}", "group_announcement": "**📢Group announcement**\n{message}",
"ping_response": "pong, this is fedigroups service v{version}" "ping_response": "pong, this is fedigroups service v{version}"

@ -12,7 +12,7 @@ use crate::error::GroupError;
use crate::group_handler::GroupHandle; use crate::group_handler::GroupHandle;
use crate::store::group_config::GroupConfig; use crate::store::group_config::GroupConfig;
use crate::store::CommonConfig; use crate::store::CommonConfig;
use crate::tr::{EMPTY_TRANSLATION_TABLE, TranslationTable}; use crate::tr::TranslationTable;
use crate::utils; use crate::utils;
use crate::utils::{normalize_acct, LogError}; use crate::utils::{normalize_acct, LogError};
@ -35,8 +35,7 @@ pub struct ProcessMention<'a> {
impl<'a> ProcessMention<'a> { impl<'a> ProcessMention<'a> {
fn tr(&self) -> &TranslationTable { fn tr(&self) -> &TranslationTable {
self.cc.tr.get(self.config.get_locale()) self.cc.tr(self.config.get_locale())
.unwrap_or(&EMPTY_TRANSLATION_TABLE)
} }
async fn lookup_acct_id(&self, acct: &str, followed: bool) -> Result<Option<String>, GroupError> { async fn lookup_acct_id(&self, acct: &str, followed: bool) -> Result<Option<String>, GroupError> {

@ -1,5 +1,4 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use std::ops::Deref;
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@ -20,7 +19,7 @@ use crate::command::StatusCommand;
use crate::error::GroupError; use crate::error::GroupError;
use crate::store::CommonConfig; use crate::store::CommonConfig;
use crate::store::GroupConfig; use crate::store::GroupConfig;
use crate::tr::{EMPTY_TRANSLATION_TABLE, TranslationTable}; use crate::tr::TranslationTable;
use crate::utils::{normalize_acct, LogError, VisExt}; use crate::utils::{normalize_acct, LogError, VisExt};
mod handle_mention; mod handle_mention;
@ -162,27 +161,31 @@ impl GroupHandle {
let socket_open_time = Instant::now(); let socket_open_time = Instant::now();
let mut last_rx = Instant::now(); let mut last_rx = Instant::now();
match self.catch_up_with_missed_notifications().await { if self.cc.max_catchup_notifs > 0 {
Ok(true) => { match self.catch_up_with_missed_notifications().await {
grp_debug!(self, "Some missed notifs handled"); Ok(true) => {
} grp_debug!(self, "Some missed notifs handled");
Ok(false) => { }
grp_debug!(self, "No notifs missed"); Ok(false) => {
} grp_debug!(self, "No notifs missed");
Err(e) => { }
grp_error!(self, "Failed to handle missed notifs: {}", e); Err(e) => {
grp_error!(self, "Failed to handle missed notifs: {}", e);
}
} }
} }
match self.catch_up_with_missed_statuses().await { if self.cc.max_catchup_statuses > 0 {
Ok(true) => { match self.catch_up_with_missed_statuses().await {
grp_debug!(self, "Some missed statuses handled"); Ok(true) => {
} grp_debug!(self, "Some missed statuses handled");
Ok(false) => { }
grp_debug!(self, "No statuses missed"); Ok(false) => {
} grp_debug!(self, "No statuses missed");
Err(e) => { }
grp_error!(self, "Failed to handle missed statuses: {}", e); Err(e) => {
grp_error!(self, "Failed to handle missed statuses: {}", e);
}
} }
} }
@ -526,8 +529,7 @@ impl GroupHandle {
} }
fn tr(&self) -> &TranslationTable { fn tr(&self) -> &TranslationTable {
self.cc.tr.get(self.config.get_locale()) self.cc.tr(self.config.get_locale())
.unwrap_or(&EMPTY_TRANSLATION_TABLE)
} }
async fn handle_new_follow(&mut self, notif_acct: &str, notif_user_id: &str) { async fn handle_new_follow(&mut self, notif_acct: &str, notif_user_id: &str) {

@ -10,12 +10,10 @@ extern crate serde;
#[macro_use] #[macro_use]
extern crate thiserror; extern crate thiserror;
use std::sync::Arc;
use clap::Arg; use clap::Arg;
use log::LevelFilter; use log::LevelFilter;
use crate::store::{NewGroupOptions, StoreOptions}; use crate::store::{NewGroupOptions, StoreOptions};
use crate::tr::TranslationTable;
use crate::utils::acct_to_server; use crate::utils::acct_to_server;
mod command; mod command;
@ -132,10 +130,8 @@ async fn main() -> anyhow::Result<()> {
store.find_locales().await; store.find_locales().await;
return Ok(());
// Start // Start
let groups = Arc::new(store).spawn_groups().await?; let groups = store.spawn_groups().await?;
let mut handles = vec![]; let mut handles = vec![];
for mut g in groups { for mut g in groups {

@ -1,9 +1,12 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::store::DEFAULT_LOCALE_NAME;
use crate::tr::TranslationTable; use crate::tr::TranslationTable;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)] #[serde(default, deny_unknown_fields)]
pub struct CommonConfig { pub struct CommonConfig {
pub groups_dir: String,
pub locales_dir: String,
/// Max number of missed notifs to process after connect /// Max number of missed notifs to process after connect
pub max_catchup_notifs: usize, pub max_catchup_notifs: usize,
/// Max number of missed statuses to process after connect /// Max number of missed statuses to process after connect
@ -31,6 +34,8 @@ pub struct CommonConfig {
impl Default for CommonConfig { impl Default for CommonConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
groups_dir: "groups".to_string(),
locales_dir: "locales".to_string(),
max_catchup_notifs: 30, max_catchup_notifs: 30,
max_catchup_statuses: 50, max_catchup_statuses: 50,
delay_fetch_page_s: 0.25, 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")
}
}
}

@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
use elefren::AppData; use elefren::AppData;
use crate::error::GroupError; use crate::error::GroupError;
use crate::store::DEFAULT_LOCALE_NAME;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)] #[serde(default, deny_unknown_fields)]
@ -75,7 +76,7 @@ impl Default for FixedConfig {
Self { Self {
enabled: true, enabled: true,
acct: "".to_string(), acct: "".to_string(),
locale: "en".to_string(), locale: DEFAULT_LOCALE_NAME.to_string(),
appdata: AppData { appdata: AppData {
base: Default::default(), base: Default::default(),
client_id: Default::default(), client_id: Default::default(),

@ -32,6 +32,9 @@ pub struct StoreOptions {
pub store_dir: String, pub store_dir: String,
} }
const DEFAULT_LOCALE_NAME : &str = "en";
const DEFAULT_LOCALE_JSON : &str = include_str!("../../locales/en.json");
impl ConfigStore { impl ConfigStore {
/// Create a new instance of the store. /// Create a new instance of the store.
/// If a path is given, it will try to load the content from a file. /// 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); 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() { if !groups_path.exists() {
debug!("Creating groups directory"); debug!("Creating groups directory");
tokio::fs::create_dir_all(&groups_path).await?; 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 { Ok(Self {
store_path: base_dir.to_owned(), store_path: base_dir.to_owned(),
@ -107,7 +127,7 @@ impl ConfigStore {
let group_dir = self.groups_path.join(&opts.acct); 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 // save & persist
@ -149,7 +169,7 @@ impl ConfigStore {
config.set_appdata(appdata); config.set_appdata(appdata);
config.save_if_needed(true).await?; config.save_if_needed(true).await?;
let group_account = match client.verify_credentials().await { let _group_account = match client.verify_credentials().await {
Ok(account) => { Ok(account) => {
info!( info!(
"Group account verified: @{}, \"{}\"", "Group account verified: @{}, \"{}\"",
@ -167,6 +187,9 @@ impl ConfigStore {
} }
pub async fn find_locales(&mut self) { 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() { if !self.locales_path.is_dir() {
debug!("No locales path set!"); debug!("No locales path set!");
return; return;
@ -175,7 +198,7 @@ impl ConfigStore {
let entries = match std::fs::read_dir(&self.locales_path) { let entries = match std::fs::read_dir(&self.locales_path) {
Ok(ee) => ee, Ok(ee) => ee,
Err(e) => { Err(e) => {
warn!("Error listing locales"); warn!("Error listing locales: {}", e);
return; return;
} }
}; };
@ -189,13 +212,8 @@ impl ConfigStore {
match tokio::fs::read(&path).await { match tokio::fs::read(&path).await {
Ok(f) => { Ok(f) => {
if let Ok(tr) = serde_json::from_slice::<TranslationTable>(&f) { let locale_name = path.file_stem().unwrap_or_default().to_string_lossy();
let locale_name = path.file_stem().unwrap_or_default().to_string_lossy().to_string(); self.load_locale(&locale_name, &String::from_utf8_lossy(&f), false);
debug!("Loaded locale: {}", locale_name);
self.config.tr.insert(locale_name, tr);
} else {
error!("Failed to parse locale file {}", path.display());
}
}, },
Err(e) => { Err(e) => {
error!("Failed to read locale file {}: {}", path.display(), 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::<TranslationTable>(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 /// Spawn existing group using saved creds
pub async fn spawn_groups(self) -> Result<Vec<GroupHandle>, GroupError> { pub async fn spawn_groups(self) -> Result<Vec<GroupHandle>, GroupError> {
info!("Starting group services for groups in {}", self.groups_path.display()); info!("Starting group services for groups in {}", self.groups_path.display());

@ -1,48 +1,50 @@
//! magic for custom translations and strings //! magic for custom translations and strings
use std::collections::HashMap; use std::collections::HashMap;
use once_cell::sync::Lazy;
#[derive(Debug,Clone,Serialize,Deserialize,Default)] #[derive(Debug,Clone,Serialize,Deserialize,Default)]
pub struct TranslationTable { pub struct TranslationTable {
#[serde(flatten)] #[serde(flatten)]
entries: Option<HashMap<String, String>>, entries: HashMap<String, String>,
} }
pub const EMPTY_TRANSLATION_TABLE : TranslationTable = TranslationTable {
entries: None,
};
impl TranslationTable { impl TranslationTable {
#[allow(unused)]
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
/// Iterate all entries
pub fn entries(&self) -> impl Iterator<Item=(&String, &String)> {
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) { pub fn add_translation(&mut self, key : impl ToString, subs : impl ToString) {
if self.entries.is_none() { self.entries.insert(key.to_string(), subs.to_string());
self.entries = Some(Default::default()); }
}
self.entries.as_mut().unwrap().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 { pub fn subs(&self, key : &str, substitutions: &[&str]) -> String {
if let Some(ee) = &self.entries { match self.entries.get(key) {
match ee.get(key) { Some(s) => {
Some(s) => { // TODO optimize
// TODO optimize let mut s = s.clone();
let mut s = s.clone(); for pair in substitutions.chunks(2) {
for pair in substitutions.chunks(2) { if pair.len() != 2 {
if pair.len() != 2 { continue;
continue;
}
s = s.replace(&format!("{{{}}}", pair[0]), pair[1]);
} }
s s = s.replace(&format!("{{{}}}", pair[0]), pair[1]);
} }
None => key.to_owned() s
} }
} else { None => key.to_owned()
key.to_owned()
} }
} }
} }

Loading…
Cancel
Save