Compare commits

..

8 Commits

  1. 38
      CHANGELOG.md
  2. 2603
      Cargo.lock
  3. 24
      Cargo.toml
  4. 2
      build.rs
  5. 3
      locales/cs.json
  6. 10
      src/error.rs
  7. 33
      src/group_handler/handle_mention.rs
  8. 21
      src/group_handler/mod.rs
  9. 18
      src/main.rs
  10. 4
      src/store/common_config.rs
  11. 67
      src/store/group_config.rs
  12. 16
      src/store/mod.rs
  13. 3
      src/tr.rs
  14. 2
      src/utils.rs

@ -1,30 +1,40 @@
# Changelog # Changelog
## v0.4.3 ## v0.4.6 [2025-06-15]
- Update dependencies
## v0.4.5 [2022-11-02]
- Ignore #nobot in bio if the user is also a member
## v0.4.4 [2021-11-02]
- Fix some failing tests
- Lowercase the domain when normalizing an account
## v0.4.3 [2021-10-12]
- Fix hashtag not working in a mention - Fix hashtag not working in a mention
## v0.4.2 ## v0.4.2 [2021-10-12]
- Fix URL fragment detected as hashtag - Fix URL fragment detected as hashtag
## v0.4.1 ## v0.4.1 [2021-10-10]
- "en" translation fixes - "en" translation fixes
- add `messages.json` - add `messages.json`
- Config files are now parsed as JSON5 = comments are allowed - Config files are now parsed as JSON5 = comments are allowed
## v0.4.0 ## v0.4.0 [2021-10-10]
- Add a translation system using the `locales` folder - Add a translation system using the `locales` folder
- Add more trace logging - Add more trace logging
## v0.3.0 ## v0.3.0 [2021-10-05]
- Changed config/storage format to directory-based, removed shared config mutex - Changed config/storage format to directory-based, removed shared config mutex
- Made more options configurable (timeouts, catch-up limits, etc) - Made more options configurable (timeouts, catch-up limits, etc)
- Changed default log level to Debug, added `-q` to reduce it (opposite of `-v`) - Changed default log level to Debug, added `-q` to reduce it (opposite of `-v`)
- Code cleaning - Code cleaning
## v0.2.8 ## v0.2.8 [2021-08-30]
- fix error processing statuses when a misskey poll has infinite run time - fix error processing statuses when a misskey poll has infinite run time
## v0.2.7 ## v0.2.7 [2021-08-30]
- Fix some wrong responses to admin commands - Fix some wrong responses to admin commands
- Remove automatic announcements from some admin commands - Remove automatic announcements from some admin commands
- Send no reply to unauthorized admin commands - Send no reply to unauthorized admin commands
@ -32,34 +42,34 @@
- Add `/optout` and `/optin` - Add `/optout` and `/optin`
- Add `#nobot` checking when using the `/boost` command - Add `#nobot` checking when using the `/boost` command
## v0.2.6 ## v0.2.6 [2021-08-28]
- Allow boosting group hashtags when they are in a reply, except when it is private/DM - Allow boosting group hashtags when they are in a reply, except when it is private/DM
or contains actionable commands or contains actionable commands
- `/follow` and `/unfollow` are now aliases to `/add` and `/remove` (for users and tags) - `/follow` and `/unfollow` are now aliases to `/add` and `/remove` (for users and tags)
- Add workaround for pleroma markdown processor eating trailing hashtags - Add workaround for pleroma markdown processor eating trailing hashtags
- Command replies are now always DM again so we don't spam timelines - Command replies are now always DM again so we don't spam timelines
## v0.2.5 ## v0.2.5 [2021-08-27]
- Add `/undo` command - Add `/undo` command
- Fix users joining via follow not marked as members - Fix users joining via follow not marked as members
## v0.2.4 ## v0.2.4 [2021-08-27]
- make account lookup try harder - make account lookup try harder
## v0.2.3 ## v0.2.3 [2021-08-27]
- `/add user` will now try to follow even if already a member - `/add user` will now try to follow even if already a member
## v0.2.2 ## v0.2.2 [2021-08-27]
- All hashtags, server names and handles are now lowercased = case-insensitive - All hashtags, server names and handles are now lowercased = case-insensitive
- Prevent the `-a` flag overwriting existing group in the config - Prevent the `-a` flag overwriting existing group in the config
- Update the help text - Update the help text
- `/i` now works in hashtag posts - `/i` now works in hashtag posts
- `/add user` and `/remove user` now correctly follow/unfollow - `/add user` and `/remove user` now correctly follow/unfollow
## v0.2.1 ## v0.2.1 [2021-08-27]
- More reliable websocket reconnect, workaround for pleroma socket going silent - More reliable websocket reconnect, workaround for pleroma socket going silent
## v0.2.0 ## v0.2.0 [2021-08-27]
- Add hashtag boosting and back-follow/unfollow - Add hashtag boosting and back-follow/unfollow
- Add hashtag commands - Add hashtag commands
- Code reorganization - Code reorganization

2603
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
[package] [package]
name = "fedigroups" name = "fedigroups"
version = "0.4.3" version = "0.4.6"
authors = ["Ondřej Hruška <ondra@ondrovo.com>"] authors = ["Ondřej Hruška <ondra@ondrovo.com>"]
edition = "2018" edition = "2018"
publish = false publish = false
@ -9,24 +9,20 @@ build = "build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
#elefren = { path = "../elefren22-fork" } #elefren = { path = "../elefren-fork" }
elefren = { git = "https://git.ondrovo.com/MightyPork/elefren-fork.git", rev = "b10e5935ae32f4756b19e9ca58b78a5382f865d1" } elefren = { git = "https://git.ondrovo.com/MightyPork/elefren-fork.git", rev = "8aa32c060eddb825e46f777661a93ff6f745659d" }
env_logger = "0.9.0" env_logger = "0.11.8"
log = "0.4.14" log = "0.4.27"
serde = "1" serde = "1"
serde_json = "1" serde_json = "1"
anyhow = "1" anyhow = "1"
clap = "2.33.0" clap = "3.2"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
tokio-stream = "0.1.7" thiserror = "2.0.12"
thiserror = "1.0.26"
futures = "0.3" futures = "0.3"
voca_rs = "1.13.0" voca_rs = "1.15.2"
regex = "1.5.4" regex = "1.11.1"
once_cell = "1.8.0" once_cell = "1.21.3"
json5 = "0.4.1" json5 = "0.4.1"
native-tls = "0.2.8"
websocket = "0.26.2"

@ -2,7 +2,7 @@ use std::process::Command;
use std::str; use std::str;
fn main() { fn main() {
let desc_c = Command::new("git").args(&["describe", "--all", "--long"]).output().unwrap(); let desc_c = Command::new("git").args(["describe", "--all", "--long"]).output().unwrap();
let desc = unsafe { str::from_utf8_unchecked(&desc_c.stdout) }; let desc = unsafe { str::from_utf8_unchecked(&desc_c.stdout) };

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

@ -8,6 +8,8 @@ pub enum GroupError {
UserIsBanned, UserIsBanned,
#[error("User opted out from the group")] #[error("User opted out from the group")]
UserOptedOut, UserOptedOut,
#[error("User opted out from the group using #nobot")]
UserOptedOutNobot,
#[error("Server could not be banned because there are admin users on it")] #[error("Server could not be banned because there are admin users on it")]
AdminsOnServer, AdminsOnServer,
#[error("Config error: {0}")] #[error("Config error: {0}")]
@ -21,7 +23,13 @@ pub enum GroupError {
#[error(transparent)] #[error(transparent)]
Serializer5(#[from] json5::Error), Serializer5(#[from] json5::Error),
#[error(transparent)] #[error(transparent)]
Elefren(#[from] elefren::Error), Elefren(Box<elefren::Error>),
}
impl From<elefren::Error> for GroupError {
fn from(e: elefren::Error) -> Self {
Self::Elefren(Box::new(e))
}
} }
// this is for tests // this is for tests

@ -2,25 +2,21 @@ use std::cmp::Ordering;
use std::collections::HashSet; use std::collections::HashSet;
use std::time::Duration; use std::time::Duration;
use elefren::{FediClient, SearchType, StatusBuilder};
use elefren::entities::account::Account; use elefren::entities::account::Account;
use elefren::entities::prelude::Status; use elefren::entities::prelude::Status;
use elefren::status_builder::Visibility; use elefren::status_builder::Visibility;
use elefren::{FediClient, SearchType, StatusBuilder};
use crate::command::{RE_NOBOT_TAG, StatusCommand}; use crate::command::{StatusCommand, RE_NOBOT_TAG};
use crate::error::GroupError; use crate::error::GroupError;
use crate::group_handler::GroupHandle; use crate::group_handler::GroupHandle;
use crate::store::CommonConfig;
use crate::store::group_config::GroupConfig; use crate::store::group_config::GroupConfig;
use crate::store::CommonConfig;
use crate::tr::TranslationTable; use crate::tr::TranslationTable;
use crate::utils; use crate::utils;
use crate::utils::{LogError, normalize_acct, VisExt}; use crate::utils::{normalize_acct, LogError, VisExt};
use crate::{ use crate::{grp_debug, grp_info, grp_warn};
grp_debug,
grp_warn,
grp_info
};
pub struct ProcessMention<'a> { pub struct ProcessMention<'a> {
status: Status, status: Status,
@ -149,8 +145,7 @@ impl<'a> ProcessMention<'a> {
} }
async fn reblog_status(&self) { async fn reblog_status(&self) {
self.client.reblog(&self.status.id) self.client.reblog(&self.status.id).await.log_error("Failed to reblog status");
.await.log_error("Failed to reblog status");
self.delay_after_post().await; self.delay_after_post().await;
} }
@ -675,7 +670,11 @@ impl<'a> ProcessMention<'a> {
}; };
if self.config.is_member_only() { if self.config.is_member_only() {
self.add_reply(crate::tr!(self, "help_group_info_closed", membership = &membership_line)); self.add_reply(crate::tr!(
self,
"help_group_info_closed",
membership = &membership_line
));
} else { } else {
self.add_reply(crate::tr!(self, "help_group_info_open", membership = &membership_line)); self.add_reply(crate::tr!(self, "help_group_info_open", membership = &membership_line));
} }
@ -774,11 +773,11 @@ impl<'a> ProcessMention<'a> {
// Try to unfollow // Try to unfollow
let account = self.client.get_account(id).await?; let account = self.client.get_account(id).await?;
let bio = utils::strip_html(&account.note); let bio = utils::strip_html(&account.note);
if RE_NOBOT_TAG.is_match(&bio) {
// #nobot
Err(GroupError::UserOptedOut)
} else {
let normalized = normalize_acct(&account.acct, &self.group_acct)?; let normalized = normalize_acct(&account.acct, &self.group_acct)?;
if RE_NOBOT_TAG.is_match(&bio) && !self.config.is_member(&normalized) {
// #nobot in a non-member account
Err(GroupError::UserOptedOutNobot)
} else {
if self.config.is_banned(&normalized) { if self.config.is_banned(&normalized) {
Err(GroupError::UserIsBanned) Err(GroupError::UserIsBanned)
} else if self.config.is_optout(&normalized) { } else if self.config.is_optout(&normalized) {
@ -844,7 +843,7 @@ fn smart_split(msg: &str, prefix: Option<String>, limit: usize) -> Vec<String> {
while this_piece.len() > limit { while this_piece.len() > limit {
// line too long, try splitting at the last space, if any // line too long, try splitting at the last space, if any
let to_send = if let Some(last_space) = (&this_piece[..=limit]).rfind(' ') { let to_send = if let Some(last_space) = this_piece[..=limit].rfind(' ') {
// println!("line split at word boundary"); // println!("line split at word boundary");
let mut p = this_piece.split_off(last_space + 1); let mut p = this_piece.split_off(last_space + 1);
std::mem::swap(&mut p, &mut this_piece); std::mem::swap(&mut p, &mut this_piece);

@ -34,19 +34,11 @@ pub struct GroupHandle {
pub internal: GroupInternal, pub internal: GroupInternal,
} }
#[derive(Debug)] #[derive(Debug, Default)]
pub struct GroupInternal { pub struct GroupInternal {
recently_seen_notif_statuses: VecDeque<String>, recently_seen_notif_statuses: VecDeque<String>,
} }
impl Default for GroupInternal {
fn default() -> Self {
Self {
recently_seen_notif_statuses: VecDeque::new()
}
}
}
#[macro_export] #[macro_export]
macro_rules! grp_debug { macro_rules! grp_debug {
($self:ident, $f:expr) => { ($self:ident, $f:expr) => {
@ -138,11 +130,11 @@ impl GroupHandle {
match self.run_internal().await { match self.run_internal().await {
Ok(()) => unreachable!(), Ok(()) => unreachable!(),
Err(e @ GroupError::BadConfig(_)) => { Err(e @ GroupError::BadConfig(_)) => {
grp_error!(self, "ERROR in group handler, aborting! {}", e); grp_error!(self, "ERROR in group handler, aborting! {:?}", e);
return Err(e); return Err(e);
} }
Err(other) => { Err(other) => {
grp_error!(self, "ERROR in group handler, will restart! {}", other); grp_error!(self, "ERROR in group handler, will restart! {:?}", other);
tokio::time::sleep(Duration::from_secs_f64(self.cc.delay_reopen_error_s)).await; tokio::time::sleep(Duration::from_secs_f64(self.cc.delay_reopen_error_s)).await;
} }
} }
@ -175,7 +167,7 @@ impl GroupHandle {
grp_debug!(self, "No notifs missed"); grp_debug!(self, "No notifs missed");
} }
Err(e) => { Err(e) => {
grp_error!(self, "Failed to handle missed notifs: {}", e); grp_error!(self, "Failed to handle missed notifs: {:?}", e);
} }
} }
} }
@ -189,7 +181,7 @@ impl GroupHandle {
grp_debug!(self, "No statuses missed"); grp_debug!(self, "No statuses missed");
} }
Err(e) => { Err(e) => {
grp_error!(self, "Failed to handle missed statuses: {}", e); grp_error!(self, "Failed to handle missed statuses: {:?}", e);
} }
} }
} }
@ -552,8 +544,7 @@ impl GroupHandle {
self.config.set_member(notif_acct, true).log_error("Fail add a member"); self.config.set_member(notif_acct, true).log_error("Fail add a member");
crate::tr!(self, "mention_prefix", user = notif_acct) crate::tr!(self, "mention_prefix", user = notif_acct) + &crate::tr!(self, "welcome_public")
+ &crate::tr!(self, "welcome_public")
}; };
let post = StatusBuilder::new() let post = StatusBuilder::new()

@ -31,26 +31,26 @@ async fn main() -> anyhow::Result<()> {
let args = clap::App::new("groups") let args = clap::App::new("groups")
.arg( .arg(
Arg::with_name("verbose") Arg::with_name("verbose")
.short("v") .short('v')
.multiple(true) .multiple_occurrences(true)
.help("increase logging, can be repeated"), .help("increase logging, can be repeated"),
) )
.arg( .arg(
Arg::with_name("quiet") Arg::with_name("quiet")
.short("q") .short('q')
.multiple(true) .multiple_occurrences(true)
.help("decrease logging, can be repeated"), .help("decrease logging, can be repeated"),
) )
.arg( .arg(
Arg::with_name("config") Arg::with_name("config")
.short("c") .short('c')
.long("config") .long("config")
.takes_value(true) .takes_value(true)
.help("set custom config directory, defaults to the current folder"), .help("set custom config directory, defaults to the current folder"),
) )
.arg( .arg(
Arg::with_name("auth") Arg::with_name("auth")
.short("a") .short('a')
.long("auth") .long("auth")
.takes_value(true) .takes_value(true)
.value_name("HANDLE") .value_name("HANDLE")
@ -58,7 +58,7 @@ async fn main() -> anyhow::Result<()> {
) )
.arg( .arg(
Arg::with_name("reauth") Arg::with_name("reauth")
.short("A") .short('A')
.long("reauth") .long("reauth")
.takes_value(true) .takes_value(true)
.value_name("HANDLE") .value_name("HANDLE")
@ -76,9 +76,7 @@ async fn main() -> anyhow::Result<()> {
let default_level = 3; let default_level = 3;
let level = ( let level = (default_level as isize + args.occurrences_of("verbose") as isize
default_level as isize
+ args.occurrences_of("verbose") as isize
- args.occurrences_of("quiet") as isize) - args.occurrences_of("quiet") as isize)
.clamp(0, LEVELS.len() as isize) as usize; .clamp(0, LEVELS.len() as isize) as usize;

@ -1,6 +1,6 @@
use std::collections::HashMap;
use crate::store::DEFAULT_LOCALE_NAME; use crate::store::DEFAULT_LOCALE_NAME;
use crate::tr::TranslationTable; use crate::tr::TranslationTable;
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)] #[serde(default, deny_unknown_fields)]
@ -55,7 +55,7 @@ impl CommonConfig {
pub fn tr(&self, lang: &str) -> &TranslationTable { pub fn tr(&self, lang: &str) -> &TranslationTable {
match self.tr.get(lang) { match self.tr.get(lang) {
Some(tr) => tr, Some(tr) => tr,
None => self.tr.get(DEFAULT_LOCALE_NAME).expect("default locale is not loaded") None => self.tr.get(DEFAULT_LOCALE_NAME).expect("default locale is not loaded"),
} }
} }
} }

@ -4,7 +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, CommonConfig}; use crate::store::{CommonConfig, DEFAULT_LOCALE_NAME};
use crate::tr::TranslationTable; use crate::tr::TranslationTable;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -25,7 +25,7 @@ struct FixedConfig {
_path: PathBuf, _path: PathBuf,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)] #[serde(default, deny_unknown_fields)]
struct MutableConfig { struct MutableConfig {
/// Hashtags the group will auto-boost from it's members /// Hashtags the group will auto-boost from it's members
@ -48,7 +48,7 @@ struct MutableConfig {
_path: PathBuf, _path: PathBuf,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)] #[serde(default, deny_unknown_fields)]
struct StateConfig { struct StateConfig {
/// Last seen notification timestamp (millis) /// Last seen notification timestamp (millis)
@ -94,33 +94,6 @@ impl Default for FixedConfig {
} }
} }
impl Default for MutableConfig {
fn default() -> Self {
Self {
group_tags: Default::default(),
admin_users: Default::default(),
member_users: Default::default(),
banned_users: Default::default(),
optout_users: Default::default(),
member_only: false,
banned_servers: Default::default(),
_dirty: false,
_path: PathBuf::default(),
}
}
}
impl Default for StateConfig {
fn default() -> Self {
Self {
last_notif_ts: 0,
last_status_ts: 0,
_dirty: false,
_path: PathBuf::default(),
}
}
}
macro_rules! impl_change_tracking { macro_rules! impl_change_tracking {
($struc:ident) => { ($struc:ident) => {
impl $struc { impl $struc {
@ -259,7 +232,11 @@ impl GroupConfig {
} }
/// (re)init using new authorization /// (re)init using new authorization
pub(crate) async fn initialize_by_appdata(acct: String, appdata: AppData, group_dir: PathBuf) -> Result<(), GroupError> { pub(crate) async fn initialize_by_appdata(
acct: String,
appdata: AppData,
group_dir: PathBuf,
) -> Result<(), GroupError> {
if !group_dir.is_dir() { if !group_dir.is_dir() {
debug!("Creating group directory"); debug!("Creating group directory");
tokio::fs::create_dir_all(&group_dir).await?; tokio::fs::create_dir_all(&group_dir).await?;
@ -306,7 +283,12 @@ impl GroupConfig {
/* state */ /* state */
let state = load_or_create_state_file(state_path).await?; let state = load_or_create_state_file(state_path).await?;
let g = GroupConfig { config, control, state, _group_tr: TranslationTable::new() }; let g = GroupConfig {
config,
control,
state,
_group_tr: TranslationTable::new(),
};
g.warn_of_bad_config(); g.warn_of_bad_config();
Ok(()) Ok(())
} }
@ -338,7 +320,12 @@ impl GroupConfig {
} }
} }
let g = GroupConfig { config, control, state, _group_tr: tr }; let g = GroupConfig {
config,
control,
state,
_group_tr: tr,
};
g.warn_of_bad_config(); g.warn_of_bad_config();
Ok(g) Ok(g)
} }
@ -458,7 +445,7 @@ impl GroupConfig {
/// Check if the user's server is banned /// Check if the user's server is banned
fn is_users_server_banned(&self, acct: &str) -> bool { fn is_users_server_banned(&self, acct: &str) -> bool {
let server = acct_to_server(acct); let server = acct_to_server(acct);
self.is_server_banned(server) self.is_server_banned(&server)
} }
pub(crate) fn can_write(&self, acct: &str) -> bool { pub(crate) fn can_write(&self, acct: &str) -> bool {
@ -574,8 +561,8 @@ impl GroupConfig {
} }
} }
fn acct_to_server(acct: &str) -> &str { fn acct_to_server(acct: &str) -> String {
acct.split('@').nth(1).unwrap_or_default() crate::utils::acct_to_server(acct).unwrap_or_default()
} }
#[cfg(test)] #[cfg(test)]
@ -588,15 +575,15 @@ mod tests {
config: Default::default(), config: Default::default(),
control: Default::default(), control: Default::default(),
state: Default::default(), state: Default::default(),
_group_tr: Default::default() _group_tr: Default::default(),
} }
} }
#[test] #[test]
fn test_acct_to_server() { fn test_acct_to_server() {
assert_eq!("pikachu.rocks", acct_to_server("raichu@pikachu.rocks")); assert_eq!("pikachu.rocks".to_string(), acct_to_server("raichu@pikachu.rocks"));
assert_eq!("pikachu.rocks", acct_to_server("m@pikachu.rocks")); assert_eq!("pikachu.rocks".to_string(), acct_to_server("m@pikachu.rocks"));
assert_eq!("", acct_to_server("what")); assert_eq!("".to_string(), acct_to_server("what"));
} }
#[test] #[test]

@ -9,9 +9,9 @@ use crate::group_handler::{GroupHandle, GroupInternal};
pub mod common_config; pub mod common_config;
pub mod group_config; pub mod group_config;
use crate::tr::TranslationTable;
pub use common_config::CommonConfig; pub use common_config::CommonConfig;
pub use group_config::GroupConfig; pub use group_config::GroupConfig;
use crate::tr::TranslationTable;
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct ConfigStore { pub struct ConfigStore {
@ -149,7 +149,7 @@ impl ConfigStore {
/// Re-auth an existing group /// Re-auth an existing group
pub async fn reauth_group(&self, acct: &str) -> Result<(), GroupError> { pub async fn reauth_group(&self, acct: &str) -> Result<(), GroupError> {
let group_dir = self.groups_path.join(&acct); let group_dir = self.groups_path.join(acct);
let mut config = GroupConfig::from_dir(group_dir, &self.config).await?; let mut config = GroupConfig::from_dir(group_dir, &self.config).await?;
@ -203,8 +203,7 @@ impl ConfigStore {
} }
}; };
for e in entries { for e in entries.flatten() {
if let Ok(e) = e {
let path = e.path(); let path = e.path();
if path.is_file() && path.extension().unwrap_or_default().to_string_lossy() == "json" { if path.is_file() && path.extension().unwrap_or_default().to_string_lossy() == "json" {
let filename = path.file_name().unwrap_or_default().to_string_lossy(); let filename = path.file_name().unwrap_or_default().to_string_lossy();
@ -214,7 +213,7 @@ impl ConfigStore {
Ok(f) => { Ok(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();
self.load_locale(&locale_name, &String::from_utf8_lossy(&f), false); self.load_locale(&locale_name, &String::from_utf8_lossy(&f), false);
}, }
Err(e) => { Err(e) => {
error!("Failed to read locale file {}: {}", path.display(), e); error!("Failed to read locale file {}: {}", path.display(), e);
} }
@ -222,7 +221,6 @@ impl ConfigStore {
} }
} }
} }
}
fn load_locale(&mut self, locale_name: &str, locale_json: &str, is_default: bool) { fn load_locale(&mut self, locale_name: &str, locale_json: &str, is_default: bool) {
if let Ok(mut tr) = json5::from_str::<TranslationTable>(locale_json) { if let Ok(mut tr) = json5::from_str::<TranslationTable>(locale_json) {
@ -234,10 +232,12 @@ impl ConfigStore {
for (k, v) in def_tr.entries() { for (k, v) in def_tr.entries() {
if !tr.translation_exists(k) { if !tr.translation_exists(k) {
if self.config.validate_locales { if self.config.validate_locales {
warn!("locale \"{}\" is missing \"{}\", default: {:?}", warn!(
"locale \"{}\" is missing \"{}\", default: {:?}",
locale_name, locale_name,
k, k,
def_tr.get_translation_raw(k).unwrap()); def_tr.get_translation_raw(k).unwrap()
);
} }
tr.add_translation(k, v); tr.add_translation(k, v);
} }

@ -45,7 +45,7 @@ impl TranslationTable {
} }
s s
} }
None => key.to_owned() None => key.to_owned(),
} }
} }
} }
@ -61,7 +61,6 @@ mod tests {
assert_eq!("xxx", tr.subs("xxx", &[])); assert_eq!("xxx", tr.subs("xxx", &[]));
} }
#[test] #[test]
fn subs() { fn subs() {
let mut tr = TranslationTable::new(); let mut tr = TranslationTable::new();

@ -95,6 +95,8 @@ mod test {
pub trait VisExt: Copy { pub trait VisExt: Copy {
/// Check if is private or direct /// Check if is private or direct
fn is_private(self) -> bool; fn is_private(self) -> bool;
#[allow(unused)]
fn make_unlisted(self) -> Self; fn make_unlisted(self) -> Self;
} }

Loading…
Cancel
Save