7 Commits
11 changed files with 165 additions and 112 deletions
+7
View File
@@ -1,5 +1,12 @@
# Changelog # Changelog
## v0.4.4
- Fix some failing tests
- Lowercase the domain when normalizing an account
## v0.4.3
- Fix hashtag not working in a mention
## v0.4.2 ## v0.4.2
- Fix URL fragment detected as hashtag - Fix URL fragment detected as hashtag
Generated
+1 -1
View File
@@ -328,7 +328,7 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
[[package]] [[package]]
name = "fedigroups" name = "fedigroups"
version = "0.4.2" version = "0.4.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "fedigroups" name = "fedigroups"
version = "0.4.2" version = "0.4.4"
authors = ["Ondřej Hruška <ondra@ondrovo.com>"] authors = ["Ondřej Hruška <ondra@ondrovo.com>"]
edition = "2018" edition = "2018"
publish = false publish = false
+82 -56
View File
@@ -14,7 +14,9 @@ use crate::store::group_config::GroupConfig;
use crate::store::CommonConfig; use crate::store::CommonConfig;
use crate::tr::TranslationTable; use crate::tr::TranslationTable;
use crate::utils; use crate::utils;
use crate::utils::{normalize_acct, LogError}; use crate::utils::{normalize_acct, LogError, VisExt};
use crate::{grp_debug, grp_info, grp_warn};
pub struct ProcessMention<'a> { pub struct ProcessMention<'a> {
status: Status, status: Status,
@@ -38,7 +40,7 @@ impl<'a> ProcessMention<'a> {
} }
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> {
debug!("Looking up user ID by acct: {}", acct); grp_debug!(self, "Looking up user ID by acct: {}", acct);
match tokio::time::timeout( match tokio::time::timeout(
Duration::from_secs(5), Duration::from_secs(5),
@@ -48,7 +50,7 @@ impl<'a> ProcessMention<'a> {
.await .await
{ {
Err(_) => { Err(_) => {
warn!("Account lookup timeout!"); grp_warn!(self, "Account lookup timeout!");
Err(GroupError::ApiTimeout) Err(GroupError::ApiTimeout)
} }
Ok(Err(e)) => { Ok(Err(e)) => {
@@ -60,14 +62,14 @@ impl<'a> ProcessMention<'a> {
// XXX limit is 1! // XXX limit is 1!
let acct_normalized = normalize_acct(&item.acct, &self.group_acct)?; let acct_normalized = normalize_acct(&item.acct, &self.group_acct)?;
if acct_normalized == acct { if acct_normalized == acct {
debug!("Search done, account found: {}", item.acct); grp_debug!(self, "Search done, account found: {}", item.acct);
return Ok(Some(item.id)); return Ok(Some(item.id));
} else { } else {
warn!("Found wrong account: {}", item.acct); grp_warn!(self, "Found wrong account: {}", item.acct);
} }
} }
debug!("Search done, nothing found"); grp_debug!(self, "Search done, nothing found");
Ok(None) Ok(None)
} }
} }
@@ -101,14 +103,14 @@ impl<'a> ProcessMention<'a> {
} }
async fn follow_user_by_id(&self, id: &str) -> Result<(), GroupError> { async fn follow_user_by_id(&self, id: &str) -> Result<(), GroupError> {
debug!("Trying to follow user #{}", id); grp_debug!(self, "Trying to follow user #{}", id);
self.client.follow(id).await?; self.client.follow(id).await?;
self.delay_after_post().await; self.delay_after_post().await;
Ok(()) Ok(())
} }
async fn unfollow_user_by_id(&self, id: &str) -> Result<(), GroupError> { async fn unfollow_user_by_id(&self, id: &str) -> Result<(), GroupError> {
debug!("Trying to unfollow user #{}", id); grp_debug!(self, "Trying to unfollow user #{}", id);
self.client.unfollow(id).await?; self.client.unfollow(id).await?;
self.delay_after_post().await; self.delay_after_post().await;
Ok(()) Ok(())
@@ -119,7 +121,7 @@ impl<'a> ProcessMention<'a> {
let status_acct = normalize_acct(&status.account.acct, &group_acct)?.to_string(); let status_acct = normalize_acct(&status.account.acct, &group_acct)?.to_string();
if gh.config.is_banned(&status_acct) { if gh.config.is_banned(&status_acct) {
warn!("Status author {} is banned!", status_acct); grp_warn!(gh, "Status author {} is banned!", status_acct);
return Ok(()); return Ok(());
} }
@@ -143,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;
} }
@@ -163,7 +164,7 @@ impl<'a> ProcessMention<'a> {
self.handle_post_with_no_commands().await; self.handle_post_with_no_commands().await;
} else { } else {
if commands.contains(&StatusCommand::Ignore) { if commands.contains(&StatusCommand::Ignore) {
debug!("Notif ignored because of ignore command"); grp_debug!(self, "Notif ignored because of ignore command");
return Ok(()); return Ok(());
} }
@@ -259,7 +260,7 @@ impl<'a> ProcessMention<'a> {
self.delay_after_post().await; self.delay_after_post().await;
} }
Err(e) => { Err(e) => {
warn!("Can't reblog: {}", e); grp_warn!(self, "Can't reblog: {}", e);
} }
} }
} }
@@ -267,9 +268,9 @@ impl<'a> ProcessMention<'a> {
if !self.replies.is_empty() { if !self.replies.is_empty() {
let mut msg = std::mem::take(&mut self.replies); let mut msg = std::mem::take(&mut self.replies);
debug!("r={}", msg); grp_debug!(self, "r={}", msg);
apply_trailing_hashtag_pleroma_bug_workaround(&mut msg); self.apply_trailing_hashtag_pleroma_bug_workaround(&mut msg);
let mention = crate::tr!(self, "mention_prefix", user = &self.status_acct); let mention = crate::tr!(self, "mention_prefix", user = &self.status_acct);
self.send_reply_multipart(mention, msg).await?; self.send_reply_multipart(mention, msg).await?;
@@ -277,9 +278,9 @@ impl<'a> ProcessMention<'a> {
if !self.announcements.is_empty() { if !self.announcements.is_empty() {
let mut msg = std::mem::take(&mut self.announcements); let mut msg = std::mem::take(&mut self.announcements);
debug!("a={}", msg); grp_debug!(self, "a={}", msg);
apply_trailing_hashtag_pleroma_bug_workaround(&mut msg); self.apply_trailing_hashtag_pleroma_bug_workaround(&mut msg);
let msg = crate::tr!(self, "group_announcement", message = &msg); let msg = crate::tr!(self, "group_announcement", message = &msg);
self.send_announcement_multipart(&msg).await?; self.send_announcement_multipart(&msg).await?;
@@ -339,24 +340,45 @@ impl<'a> ProcessMention<'a> {
} }
async fn handle_post_with_no_commands(&mut self) { async fn handle_post_with_no_commands(&mut self) {
debug!("No commands in post"); grp_debug!(self, "No commands in post");
if self.status.in_reply_to_id.is_none() {
if self.status.visibility.is_private() {
grp_debug!(self, "Mention is private, discard");
return;
}
if self.can_write { if self.can_write {
if self.status.in_reply_to_id.is_none() {
// Someone tagged the group in OP, boost it. // Someone tagged the group in OP, boost it.
info!("Boosting OP mention"); grp_info!(self, "Boosting OP mention");
// tokio::time::sleep(DELAY_BEFORE_ACTION).await; // tokio::time::sleep(DELAY_BEFORE_ACTION).await;
self.reblog_status().await; self.reblog_status().await;
} else {
warn!("User @{} can't post to group!", self.status_acct);
}
// Otherwise, don't react // Otherwise, don't react
} else { } else {
debug!("Not OP, ignore mention"); // Check for tags
let tags = crate::command::parse_status_tags(&self.status.content);
grp_debug!(self, "Tags in mention: {:?}", tags);
for t in tags {
if self.config.is_tag_followed(&t) {
grp_info!(self, "REBLOG #{} STATUS", t);
self.client.reblog(&self.status.id).await.log_error("Failed to reblog");
self.delay_after_post().await;
return;
} else {
grp_debug!(self, "#{} is not a group tag", t);
}
}
grp_debug!(self, "Not OP & no tags, ignore mention");
}
} else {
grp_warn!(self, "User @{} can't post to group!", self.status_acct);
} }
} }
async fn cmd_announce(&mut self, msg: String) { async fn cmd_announce(&mut self, msg: String) {
info!("Sending PSA"); grp_info!(self, "Sending PSA");
self.add_announcement(msg); self.add_announcement(msg);
} }
@@ -364,7 +386,7 @@ impl<'a> ProcessMention<'a> {
if self.can_write { if self.can_write {
self.do_boost_prev_post = self.status.in_reply_to_id.is_some(); self.do_boost_prev_post = self.status.in_reply_to_id.is_some();
} else { } else {
warn!("User @{} can't share to group!", self.status_acct); grp_warn!(self, "User @{} can't share to group!", self.status_acct);
} }
} }
@@ -398,19 +420,19 @@ impl<'a> ProcessMention<'a> {
// This is a post sent by the group user, likely an announcement. // This is a post sent by the group user, likely an announcement.
// Undo here means delete it. // Undo here means delete it.
if self.is_admin { if self.is_admin {
info!("Deleting group post #{}", parent_status_id); grp_info!(self, "Deleting group post #{}", parent_status_id);
self.client.delete_status(parent_status_id).await?; self.client.delete_status(parent_status_id).await?;
self.delay_after_post().await; self.delay_after_post().await;
} else { } else {
warn!("Only admin can delete posts made by the group user"); grp_warn!(self, "Only admin can delete posts made by the group user");
} }
} else if self.is_admin || parent_account_id == &self.status_user_id { } else if self.is_admin || parent_account_id == &self.status_user_id {
info!("Un-reblogging post #{}", parent_status_id); grp_info!(self, "Un-reblogging post #{}", parent_status_id);
// User unboosting own post boosted by accident, or admin doing it // User unboosting own post boosted by accident, or admin doing it
self.client.unreblog(parent_status_id).await?; self.client.unreblog(parent_status_id).await?;
self.delay_after_post().await; self.delay_after_post().await;
} else { } else {
warn!("Only the author and admins can undo reblogs"); grp_warn!(self, "Only the author and admins can undo reblogs");
// XXX this means when someone /b's someone else's post to a group, // XXX this means when someone /b's someone else's post to a group,
// they then can't reverse that (only admin or the post's author can) // they then can't reverse that (only admin or the post's author can)
} }
@@ -436,7 +458,7 @@ impl<'a> ProcessMention<'a> {
self.add_reply(crate::tr!(self, "cmd_ban_user_fail_already", user = &u)); self.add_reply(crate::tr!(self, "cmd_ban_user_fail_already", user = &u));
} }
} else { } else {
warn!("Ignore cmd, user not admin"); grp_warn!(self, "Ignore cmd, user not admin");
} }
Ok(()) Ok(())
} }
@@ -458,7 +480,7 @@ impl<'a> ProcessMention<'a> {
self.add_reply(crate::tr!(self, "cmd_unban_user_fail_already", user = &u)); self.add_reply(crate::tr!(self, "cmd_unban_user_fail_already", user = &u));
} }
} else { } else {
warn!("Ignore cmd, user not admin"); grp_warn!(self, "Ignore cmd, user not admin");
} }
Ok(()) Ok(())
} }
@@ -478,7 +500,7 @@ impl<'a> ProcessMention<'a> {
self.add_reply(crate::tr!(self, "cmd_ban_server_fail_already", server = s)); self.add_reply(crate::tr!(self, "cmd_ban_server_fail_already", server = s));
} }
} else { } else {
warn!("Ignore cmd, user not admin"); grp_warn!(self, "Ignore cmd, user not admin");
} }
} }
@@ -497,7 +519,7 @@ impl<'a> ProcessMention<'a> {
self.add_reply(crate::tr!(self, "cmd_unban_server_fail_already", server = s)); self.add_reply(crate::tr!(self, "cmd_unban_server_fail_already", server = s));
} }
} else { } else {
warn!("Ignore cmd, user not admin"); grp_warn!(self, "Ignore cmd, user not admin");
} }
} }
@@ -516,7 +538,7 @@ impl<'a> ProcessMention<'a> {
} }
} }
} else { } else {
warn!("Ignore cmd, user not admin"); grp_warn!(self, "Ignore cmd, user not admin");
} }
Ok(()) Ok(())
} }
@@ -534,7 +556,7 @@ impl<'a> ProcessMention<'a> {
} }
} }
} else { } else {
warn!("Ignore cmd, user not admin"); grp_warn!(self, "Ignore cmd, user not admin");
} }
Ok(()) Ok(())
} }
@@ -548,7 +570,7 @@ impl<'a> ProcessMention<'a> {
self.add_reply(crate::tr!(self, "cmd_add_tag_fail_already", tag = &tag)); self.add_reply(crate::tr!(self, "cmd_add_tag_fail_already", tag = &tag));
} }
} else { } else {
warn!("Ignore cmd, user not admin"); grp_warn!(self, "Ignore cmd, user not admin");
} }
} }
@@ -561,7 +583,7 @@ impl<'a> ProcessMention<'a> {
self.add_reply(crate::tr!(self, "cmd_remove_tag_fail_already", tag = &tag)); self.add_reply(crate::tr!(self, "cmd_remove_tag_fail_already", tag = &tag));
} }
} else { } else {
warn!("Ignore cmd, user not admin"); grp_warn!(self, "Ignore cmd, user not admin");
} }
} }
@@ -584,7 +606,7 @@ impl<'a> ProcessMention<'a> {
self.add_reply(crate::tr!(self, "cmd_admin_fail_already", user = &u)); self.add_reply(crate::tr!(self, "cmd_admin_fail_already", user = &u));
} }
} else { } else {
warn!("Ignore cmd, user not admin"); grp_warn!(self, "Ignore cmd, user not admin");
} }
Ok(()) Ok(())
} }
@@ -605,7 +627,7 @@ impl<'a> ProcessMention<'a> {
self.add_reply(crate::tr!(self, "cmd_unadmin_fail_already", user = &u)); self.add_reply(crate::tr!(self, "cmd_unadmin_fail_already", user = &u));
} }
} else { } else {
warn!("Ignore cmd, user not admin"); grp_warn!(self, "Ignore cmd, user not admin");
} }
Ok(()) Ok(())
} }
@@ -619,7 +641,7 @@ impl<'a> ProcessMention<'a> {
self.add_reply(crate::tr!(self, "cmd_open_resp_already")); self.add_reply(crate::tr!(self, "cmd_open_resp_already"));
} }
} else { } else {
warn!("Ignore cmd, user not admin"); grp_warn!(self, "Ignore cmd, user not admin");
} }
} }
@@ -632,7 +654,7 @@ impl<'a> ProcessMention<'a> {
self.add_reply(crate::tr!(self, "cmd_close_resp_already")); self.add_reply(crate::tr!(self, "cmd_close_resp_already"));
} }
} else { } else {
warn!("Ignore cmd, user not admin"); grp_warn!(self, "Ignore cmd, user not admin");
} }
} }
@@ -648,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));
} }
@@ -701,7 +727,7 @@ impl<'a> ProcessMention<'a> {
async fn cmd_join(&mut self) { async fn cmd_join(&mut self) {
if self.config.is_member_or_admin(&self.status_acct) { if self.config.is_member_or_admin(&self.status_acct) {
debug!("Already member or admin, try to follow-back again"); grp_debug!(self, "Already member or admin, try to follow-back again");
// Already a member, so let's try to follow the user // Already a member, so let's try to follow the user
// again, maybe first time it failed // again, maybe first time it failed
self.follow_user_by_id(&self.status_user_id).await.log_error("Failed to follow"); self.follow_user_by_id(&self.status_user_id).await.log_error("Failed to follow");
@@ -765,15 +791,15 @@ impl<'a> ProcessMention<'a> {
async fn delay_after_post(&self) { async fn delay_after_post(&self) {
tokio::time::sleep(Duration::from_secs_f64(self.cc.delay_after_post_s)).await; tokio::time::sleep(Duration::from_secs_f64(self.cc.delay_after_post_s)).await;
} }
}
fn apply_trailing_hashtag_pleroma_bug_workaround(msg: &mut String) { fn apply_trailing_hashtag_pleroma_bug_workaround(&self, msg: &mut String) {
if crate::command::RE_HASHTAG_TRIGGERING_PLEROMA_BUG.is_match(msg) { if crate::command::RE_HASHTAG_TRIGGERING_PLEROMA_BUG.is_match(msg) {
// if a status ends with a hashtag, pleroma will fuck it up // if a status ends with a hashtag, pleroma will fuck it up
debug!("Adding \" .\" to fix pleroma hashtag eating bug!"); grp_debug!(self, "Adding \" .\" to fix pleroma hashtag eating bug!");
msg.push_str(" ."); msg.push_str(" .");
} }
} }
}
fn smart_split(msg: &str, prefix: Option<String>, limit: usize) -> Vec<String> { fn smart_split(msg: &str, prefix: Option<String>, limit: usize) -> Vec<String> {
let prefix = prefix.unwrap_or_default(); let prefix = prefix.unwrap_or_default();
@@ -785,29 +811,29 @@ fn smart_split(msg: &str, prefix: Option<String>, limit: usize) -> Vec<String> {
let mut parts_to_send = vec![]; let mut parts_to_send = vec![];
let mut this_piece = prefix.clone(); let mut this_piece = prefix.clone();
for l in msg.split('\n') { for l in msg.split('\n') {
println!("* Line: {:?}", l); // println!("* Line: {:?}", l);
match (this_piece.len() + l.len()).cmp(&limit) { match (this_piece.len() + l.len()).cmp(&limit) {
Ordering::Less => { Ordering::Less => {
println!("append line"); // println!("append line");
// this line still fits comfortably // this line still fits comfortably
this_piece.push_str(l); this_piece.push_str(l);
this_piece.push('\n'); this_piece.push('\n');
} }
Ordering::Equal => { Ordering::Equal => {
println!("exactly fits within limit"); // println!("exactly fits within limit");
// this line exactly reaches the limit // this line exactly reaches the limit
this_piece.push_str(l); this_piece.push_str(l);
parts_to_send.push(std::mem::take(&mut this_piece).trim().to_owned()); parts_to_send.push(std::mem::take(&mut this_piece).trim().to_owned());
this_piece.push_str(&prefix); this_piece.push_str(&prefix);
} }
Ordering::Greater => { Ordering::Greater => {
println!("too long to append (already {} + new {})", this_piece.len(), l.len()); // println!("too long to append (already {} + new {})", this_piece.len(), l.len());
// line too long to append // line too long to append
if this_piece != prefix { if this_piece != prefix {
let trimmed = this_piece.trim(); let trimmed = this_piece.trim();
if !trimmed.is_empty() { if !trimmed.is_empty() {
println!("flush buffer: {:?}", trimmed); // println!("flush buffer: {:?}", trimmed);
parts_to_send.push(trimmed.to_owned()); parts_to_send.push(trimmed.to_owned());
} }
} }
@@ -818,18 +844,18 @@ 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);
p p
} else { } else {
println!("line split at exact len (no word boundary found)"); // println!("line split at exact len (no word boundary found)");
let mut p = this_piece.split_off(limit); let mut p = this_piece.split_off(limit);
std::mem::swap(&mut p, &mut this_piece); std::mem::swap(&mut p, &mut this_piece);
p p
}; };
let part_trimmed = to_send.trim(); let part_trimmed = to_send.trim();
println!("flush buffer: {:?}", part_trimmed); // println!("flush buffer: {:?}", part_trimmed);
parts_to_send.push(part_trimmed.to_owned()); parts_to_send.push(part_trimmed.to_owned());
this_piece = format!("{}{}", prefix, this_piece.trim()); this_piece = format!("{}{}", prefix, this_piece.trim());
} }
@@ -841,7 +867,7 @@ fn smart_split(msg: &str, prefix: Option<String>, limit: usize) -> Vec<String> {
if this_piece != prefix { if this_piece != prefix {
let leftover_trimmed = this_piece.trim(); let leftover_trimmed = this_piece.trim();
if !leftover_trimmed.is_empty() { if !leftover_trimmed.is_empty() {
println!("flush buffer: {:?}", leftover_trimmed); // println!("flush buffer: {:?}", leftover_trimmed);
parts_to_send.push(leftover_trimmed.to_owned()); parts_to_send.push(leftover_trimmed.to_owned());
} }
} }
+7 -3
View File
@@ -42,11 +42,12 @@ pub struct GroupInternal {
impl Default for GroupInternal { impl Default for GroupInternal {
fn default() -> Self { fn default() -> Self {
Self { Self {
recently_seen_notif_statuses: VecDeque::new() recently_seen_notif_statuses: VecDeque::new(),
} }
} }
} }
#[macro_export]
macro_rules! grp_debug { macro_rules! grp_debug {
($self:ident, $f:expr) => { ($self:ident, $f:expr) => {
::log::debug!(concat!("(@{}) ", $f), $self.config.get_acct()); ::log::debug!(concat!("(@{}) ", $f), $self.config.get_acct());
@@ -56,6 +57,7 @@ macro_rules! grp_debug {
}; };
} }
#[macro_export]
macro_rules! grp_info { macro_rules! grp_info {
($self:ident, $f:expr) => { ($self:ident, $f:expr) => {
::log::info!(concat!("(@{}) ", $f), $self.config.get_acct()); ::log::info!(concat!("(@{}) ", $f), $self.config.get_acct());
@@ -65,6 +67,7 @@ macro_rules! grp_info {
}; };
} }
#[macro_export]
macro_rules! grp_trace { macro_rules! grp_trace {
($self:ident, $f:expr) => { ($self:ident, $f:expr) => {
::log::trace!(concat!("(@{}) ", $f), $self.config.get_acct()); ::log::trace!(concat!("(@{}) ", $f), $self.config.get_acct());
@@ -74,6 +77,7 @@ macro_rules! grp_trace {
}; };
} }
#[macro_export]
macro_rules! grp_warn { macro_rules! grp_warn {
($self:ident, $f:expr) => { ($self:ident, $f:expr) => {
::log::warn!(concat!("(@{}) ", $f), $self.config.get_acct()); ::log::warn!(concat!("(@{}) ", $f), $self.config.get_acct());
@@ -83,6 +87,7 @@ macro_rules! grp_warn {
}; };
} }
#[macro_export]
macro_rules! grp_error { macro_rules! grp_error {
($self:ident, $f:expr) => { ($self:ident, $f:expr) => {
::log::error!(concat!("(@{}) ", $f), $self.config.get_acct()); ::log::error!(concat!("(@{}) ", $f), $self.config.get_acct());
@@ -547,8 +552,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()
+2 -3
View File
@@ -18,6 +18,7 @@ use crate::utils::acct_to_server;
mod command; mod command;
mod error; mod error;
#[macro_use]
mod group_handler; mod group_handler;
mod store; mod store;
mod utils; mod utils;
@@ -75,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;
+2 -2
View File
@@ -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"),
} }
} }
} }
+28 -12
View File
@@ -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)]
@@ -259,7 +259,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 +310,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 +347,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 +472,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 +588,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 +602,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]
@@ -666,13 +680,15 @@ mod tests {
assert!(!group.is_banned("piggo@piggo.space"), "user not banned by default"); assert!(!group.is_banned("piggo@piggo.space"), "user not banned by default");
group.ban_user("piggo@piggo.space", true).unwrap(); group.ban_user("piggo@piggo.space", true).unwrap();
assert!(group.is_member("piggo@piggo.space"), "still member even if banned"); assert!(!group.is_member("piggo@piggo.space"), "banned user is kicked");
assert!(group.is_banned("piggo@piggo.space"), "banned user is banned"); assert!(group.is_banned("piggo@piggo.space"), "banned user is banned");
assert!(!group.can_write("piggo@piggo.space"), "banned member can't post"); assert!(!group.can_write("piggo@piggo.space"), "banned member can't post");
// unban // unban
group.ban_user("piggo@piggo.space", false).unwrap(); group.ban_user("piggo@piggo.space", false).unwrap();
assert!(!group.can_write("piggo@piggo.space"), "unbanned member is still kicked");
group.set_member("piggo@piggo.space", true).unwrap();
assert!(group.can_write("piggo@piggo.space"), "un-ban works"); assert!(group.can_write("piggo@piggo.space"), "un-ban works");
} }
+6 -4
View File
@@ -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 {
@@ -214,7 +214,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);
} }
@@ -234,10 +234,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);
} }
+1 -2
View File
@@ -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();
+6 -6
View File
@@ -19,8 +19,8 @@ impl<V, E: Error> LogError for Result<V, E> {
} }
} }
pub(crate) fn acct_to_server(acct: &str) -> Option<&str> { pub(crate) fn acct_to_server(acct: &str) -> Option<String> {
acct.trim_start_matches('@').split('@').nth(1) acct.trim_start_matches('@').split('@').nth(1).map(|s| s.to_lowercase())
} }
pub(crate) fn normalize_acct(acct: &str, group: &str) -> Result<String, GroupError> { pub(crate) fn normalize_acct(acct: &str, group: &str) -> Result<String, GroupError> {
@@ -45,8 +45,8 @@ mod test {
#[test] #[test]
fn test_acct_to_server() { fn test_acct_to_server() {
assert_eq!(Some("novak"), acct_to_server("pepa@novak")); assert_eq!(Some("novak".to_string()), acct_to_server("pepa@novak"));
assert_eq!(Some("banana.co.uk"), acct_to_server("@pepa@banana.co.uk")); assert_eq!(Some("banana.co.uk".to_string()), acct_to_server("@pepa@banana.co.uk"));
assert_eq!(None, acct_to_server("probably_local")); assert_eq!(None, acct_to_server("probably_local"));
} }
@@ -82,11 +82,11 @@ mod test {
); );
assert_eq!( assert_eq!(
Ok("piggo@piggo.space".into()), Ok("piggo@piggo.space".into()),
normalize_acct("piGGgo@pIggo.spaCe", "uhh") normalize_acct("piGGo@pIggo.spaCe", "uhh")
); );
assert_eq!( assert_eq!(
Ok("piggo@banana.nana".into()), Ok("piggo@banana.nana".into()),
normalize_acct("piGGgo", "foo@baNANA.nana") normalize_acct("piGGo", "foo@baNANA.nana")
); );
assert_eq!(Err(GroupError::BadConfig("_".into())), normalize_acct("piggo", "uhh")); assert_eq!(Err(GroupError::BadConfig("_".into())), normalize_acct("piggo", "uhh"));
} }