mirror of
https://git.ondrovo.com/MightyPork/group-actor.git
synced 2024-06-10 09:19:33 +00:00
Compare commits
8 commits
Author | SHA1 | Date | |
---|---|---|---|
5389031d8c | |||
e77c8157ae | |||
cb6724baab | |||
cbd3c0a575 | |||
e1ac2777f3 | |||
900f499932 | |||
ebcf12e46c | |||
0d37425c32 |
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -1,5 +1,15 @@
|
|||
# Changelog
|
||||
|
||||
## v0.4.5
|
||||
- Ignore #nobot in bio if the user is also a member
|
||||
|
||||
## 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
|
||||
- Fix URL fragment detected as hashtag
|
||||
|
||||
|
|
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -328,7 +328,7 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
|
|||
|
||||
[[package]]
|
||||
name = "fedigroups"
|
||||
version = "0.4.2"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "fedigroups"
|
||||
version = "0.4.2"
|
||||
version = "0.4.5"
|
||||
authors = ["Ondřej Hruška <ondra@ondrovo.com>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
|
|
@ -8,6 +8,8 @@ pub enum GroupError {
|
|||
UserIsBanned,
|
||||
#[error("User opted out from the group")]
|
||||
UserOptedOut,
|
||||
#[error("User opted out from the group using #nobot")]
|
||||
UserOptedOutNobot,
|
||||
#[error("Server could not be banned because there are admin users on it")]
|
||||
AdminsOnServer,
|
||||
#[error("Config error: {0}")]
|
||||
|
|
|
@ -14,7 +14,9 @@ use crate::store::group_config::GroupConfig;
|
|||
use crate::store::CommonConfig;
|
||||
use crate::tr::TranslationTable;
|
||||
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> {
|
||||
status: Status,
|
||||
|
@ -38,17 +40,17 @@ impl<'a> ProcessMention<'a> {
|
|||
}
|
||||
|
||||
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(
|
||||
Duration::from_secs(5),
|
||||
self.client
|
||||
.search_v2(acct, !followed, Some(SearchType::Accounts), Some(1), followed),
|
||||
)
|
||||
.await
|
||||
.await
|
||||
{
|
||||
Err(_) => {
|
||||
warn!("Account lookup timeout!");
|
||||
grp_warn!(self, "Account lookup timeout!");
|
||||
Err(GroupError::ApiTimeout)
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
|
@ -60,14 +62,14 @@ impl<'a> ProcessMention<'a> {
|
|||
// XXX limit is 1!
|
||||
let acct_normalized = normalize_acct(&item.acct, &self.group_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));
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
@ -92,23 +94,23 @@ impl<'a> ProcessMention<'a> {
|
|||
let mut to_add = String::new();
|
||||
for m in members {
|
||||
to_add.push_str(&if admins.contains(&m) {
|
||||
crate::tr!(self, "user_list_entry_admin", user=m)
|
||||
crate::tr!(self, "user_list_entry_admin", user = m)
|
||||
} else {
|
||||
crate::tr!(self, "user_list_entry", user=m)
|
||||
crate::tr!(self, "user_list_entry", user = m)
|
||||
});
|
||||
}
|
||||
self.add_reply(&to_add);
|
||||
}
|
||||
|
||||
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.delay_after_post().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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.delay_after_post().await;
|
||||
Ok(())
|
||||
|
@ -119,7 +121,7 @@ impl<'a> ProcessMention<'a> {
|
|||
let status_acct = normalize_acct(&status.account.acct, &group_acct)?.to_string();
|
||||
|
||||
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(());
|
||||
}
|
||||
|
||||
|
@ -143,8 +145,7 @@ impl<'a> ProcessMention<'a> {
|
|||
}
|
||||
|
||||
async fn reblog_status(&self) {
|
||||
self.client.reblog(&self.status.id)
|
||||
.await.log_error("Failed to reblog status");
|
||||
self.client.reblog(&self.status.id).await.log_error("Failed to reblog status");
|
||||
self.delay_after_post().await;
|
||||
}
|
||||
|
||||
|
@ -163,7 +164,7 @@ impl<'a> ProcessMention<'a> {
|
|||
self.handle_post_with_no_commands().await;
|
||||
} else {
|
||||
if commands.contains(&StatusCommand::Ignore) {
|
||||
debug!("Notif ignored because of ignore command");
|
||||
grp_debug!(self, "Notif ignored because of ignore command");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
@ -259,7 +260,7 @@ impl<'a> ProcessMention<'a> {
|
|||
self.delay_after_post().await;
|
||||
}
|
||||
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() {
|
||||
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);
|
||||
self.send_reply_multipart(mention, msg).await?;
|
||||
|
@ -277,9 +278,9 @@ impl<'a> ProcessMention<'a> {
|
|||
|
||||
if !self.announcements.is_empty() {
|
||||
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);
|
||||
self.send_announcement_multipart(&msg).await?;
|
||||
|
@ -339,24 +340,45 @@ impl<'a> ProcessMention<'a> {
|
|||
}
|
||||
|
||||
async fn handle_post_with_no_commands(&mut self) {
|
||||
debug!("No commands in post");
|
||||
if self.status.in_reply_to_id.is_none() {
|
||||
if self.can_write {
|
||||
grp_debug!(self, "No commands in post");
|
||||
|
||||
if self.status.visibility.is_private() {
|
||||
grp_debug!(self, "Mention is private, discard");
|
||||
return;
|
||||
}
|
||||
|
||||
if self.can_write {
|
||||
if self.status.in_reply_to_id.is_none() {
|
||||
// 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;
|
||||
self.reblog_status().await;
|
||||
// Otherwise, don't react
|
||||
} else {
|
||||
warn!("User @{} can't post to group!", self.status_acct);
|
||||
// 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");
|
||||
}
|
||||
// Otherwise, don't react
|
||||
} else {
|
||||
debug!("Not OP, ignore mention");
|
||||
grp_warn!(self, "User @{} can't post to group!", self.status_acct);
|
||||
}
|
||||
}
|
||||
|
||||
async fn cmd_announce(&mut self, msg: String) {
|
||||
info!("Sending PSA");
|
||||
grp_info!(self, "Sending PSA");
|
||||
self.add_announcement(msg);
|
||||
}
|
||||
|
||||
|
@ -364,7 +386,7 @@ impl<'a> ProcessMention<'a> {
|
|||
if self.can_write {
|
||||
self.do_boost_prev_post = self.status.in_reply_to_id.is_some();
|
||||
} else {
|
||||
warn!("User @{} can't share to group!", self.status_acct);
|
||||
grp_warn!(self, "User @{} can't share to group!", self.status_acct);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -392,25 +414,25 @@ impl<'a> ProcessMention<'a> {
|
|||
|
||||
async fn cmd_undo(&mut self) -> Result<(), GroupError> {
|
||||
if let (Some(ref parent_account_id), Some(ref parent_status_id)) =
|
||||
(&self.status.in_reply_to_account_id, &self.status.in_reply_to_id)
|
||||
(&self.status.in_reply_to_account_id, &self.status.in_reply_to_id)
|
||||
{
|
||||
if parent_account_id == &self.group_account.id {
|
||||
// This is a post sent by the group user, likely an announcement.
|
||||
// Undo here means delete it.
|
||||
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.delay_after_post().await;
|
||||
} 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 {
|
||||
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
|
||||
self.client.unreblog(parent_status_id).await?;
|
||||
self.delay_after_post().await;
|
||||
} 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,
|
||||
// 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));
|
||||
}
|
||||
} else {
|
||||
warn!("Ignore cmd, user not admin");
|
||||
grp_warn!(self, "Ignore cmd, user not admin");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -458,7 +480,7 @@ impl<'a> ProcessMention<'a> {
|
|||
self.add_reply(crate::tr!(self, "cmd_unban_user_fail_already", user = &u));
|
||||
}
|
||||
} else {
|
||||
warn!("Ignore cmd, user not admin");
|
||||
grp_warn!(self, "Ignore cmd, user not admin");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -478,7 +500,7 @@ impl<'a> ProcessMention<'a> {
|
|||
self.add_reply(crate::tr!(self, "cmd_ban_server_fail_already", server = s));
|
||||
}
|
||||
} 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));
|
||||
}
|
||||
} else {
|
||||
warn!("Ignore cmd, user not admin");
|
||||
grp_warn!(self, "Ignore cmd, user not admin");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -516,7 +538,7 @@ impl<'a> ProcessMention<'a> {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
warn!("Ignore cmd, user not admin");
|
||||
grp_warn!(self, "Ignore cmd, user not admin");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -534,7 +556,7 @@ impl<'a> ProcessMention<'a> {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
warn!("Ignore cmd, user not admin");
|
||||
grp_warn!(self, "Ignore cmd, user not admin");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -548,7 +570,7 @@ impl<'a> ProcessMention<'a> {
|
|||
self.add_reply(crate::tr!(self, "cmd_add_tag_fail_already", tag = &tag));
|
||||
}
|
||||
} 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));
|
||||
}
|
||||
} 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));
|
||||
}
|
||||
} else {
|
||||
warn!("Ignore cmd, user not admin");
|
||||
grp_warn!(self, "Ignore cmd, user not admin");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -605,7 +627,7 @@ impl<'a> ProcessMention<'a> {
|
|||
self.add_reply(crate::tr!(self, "cmd_unadmin_fail_already", user = &u));
|
||||
}
|
||||
} else {
|
||||
warn!("Ignore cmd, user not admin");
|
||||
grp_warn!(self, "Ignore cmd, user not admin");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -619,7 +641,7 @@ impl<'a> ProcessMention<'a> {
|
|||
self.add_reply(crate::tr!(self, "cmd_open_resp_already"));
|
||||
}
|
||||
} 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"));
|
||||
}
|
||||
} 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() {
|
||||
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 {
|
||||
self.add_reply(crate::tr!(self, "help_group_info_open", membership = &membership_line));
|
||||
}
|
||||
|
@ -682,7 +708,7 @@ impl<'a> ProcessMention<'a> {
|
|||
let mut to_add = String::new();
|
||||
|
||||
for t in tags {
|
||||
to_add.push_str(&crate::tr!(self, "tag_list_entry", tag=t));
|
||||
to_add.push_str(&crate::tr!(self, "tag_list_entry", tag = t));
|
||||
}
|
||||
self.add_reply(to_add);
|
||||
}
|
||||
|
@ -701,7 +727,7 @@ impl<'a> ProcessMention<'a> {
|
|||
|
||||
async fn cmd_join(&mut self) {
|
||||
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
|
||||
// again, maybe first time it failed
|
||||
self.follow_user_by_id(&self.status_user_id).await.log_error("Failed to follow");
|
||||
|
@ -747,11 +773,11 @@ impl<'a> ProcessMention<'a> {
|
|||
// Try to unfollow
|
||||
let account = self.client.get_account(id).await?;
|
||||
let bio = utils::strip_html(&account.note);
|
||||
if RE_NOBOT_TAG.is_match(&bio) {
|
||||
// #nobot
|
||||
Err(GroupError::UserOptedOut)
|
||||
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 {
|
||||
let normalized = normalize_acct(&account.acct, &self.group_acct)?;
|
||||
if self.config.is_banned(&normalized) {
|
||||
Err(GroupError::UserIsBanned)
|
||||
} else if self.config.is_optout(&normalized) {
|
||||
|
@ -765,13 +791,13 @@ impl<'a> ProcessMention<'a> {
|
|||
async fn delay_after_post(&self) {
|
||||
tokio::time::sleep(Duration::from_secs_f64(self.cc.delay_after_post_s)).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_trailing_hashtag_pleroma_bug_workaround(msg: &mut String) {
|
||||
if crate::command::RE_HASHTAG_TRIGGERING_PLEROMA_BUG.is_match(msg) {
|
||||
// if a status ends with a hashtag, pleroma will fuck it up
|
||||
debug!("Adding \" .\" to fix pleroma hashtag eating bug!");
|
||||
msg.push_str(" .");
|
||||
fn apply_trailing_hashtag_pleroma_bug_workaround(&self, msg: &mut String) {
|
||||
if crate::command::RE_HASHTAG_TRIGGERING_PLEROMA_BUG.is_match(msg) {
|
||||
// if a status ends with a hashtag, pleroma will fuck it up
|
||||
grp_debug!(self, "Adding \" .\" to fix pleroma hashtag eating bug!");
|
||||
msg.push_str(" .");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -785,29 +811,29 @@ fn smart_split(msg: &str, prefix: Option<String>, limit: usize) -> Vec<String> {
|
|||
let mut parts_to_send = vec![];
|
||||
let mut this_piece = prefix.clone();
|
||||
for l in msg.split('\n') {
|
||||
println!("* Line: {:?}", l);
|
||||
// println!("* Line: {:?}", l);
|
||||
|
||||
match (this_piece.len() + l.len()).cmp(&limit) {
|
||||
Ordering::Less => {
|
||||
println!("append line");
|
||||
// println!("append line");
|
||||
// this line still fits comfortably
|
||||
this_piece.push_str(l);
|
||||
this_piece.push('\n');
|
||||
}
|
||||
Ordering::Equal => {
|
||||
println!("exactly fits within limit");
|
||||
// println!("exactly fits within limit");
|
||||
// this line exactly reaches the limit
|
||||
this_piece.push_str(l);
|
||||
parts_to_send.push(std::mem::take(&mut this_piece).trim().to_owned());
|
||||
this_piece.push_str(&prefix);
|
||||
}
|
||||
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
|
||||
if this_piece != prefix {
|
||||
let trimmed = this_piece.trim();
|
||||
if !trimmed.is_empty() {
|
||||
println!("flush buffer: {:?}", trimmed);
|
||||
// println!("flush buffer: {:?}", trimmed);
|
||||
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 {
|
||||
// line too long, try splitting at the last space, if any
|
||||
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);
|
||||
std::mem::swap(&mut p, &mut this_piece);
|
||||
p
|
||||
} 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);
|
||||
std::mem::swap(&mut p, &mut this_piece);
|
||||
p
|
||||
};
|
||||
let part_trimmed = to_send.trim();
|
||||
println!("flush buffer: {:?}", part_trimmed);
|
||||
// println!("flush buffer: {:?}", part_trimmed);
|
||||
parts_to_send.push(part_trimmed.to_owned());
|
||||
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 {
|
||||
let leftover_trimmed = this_piece.trim();
|
||||
if !leftover_trimmed.is_empty() {
|
||||
println!("flush buffer: {:?}", leftover_trimmed);
|
||||
// println!("flush buffer: {:?}", leftover_trimmed);
|
||||
parts_to_send.push(leftover_trimmed.to_owned());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,11 +42,12 @@ pub struct GroupInternal {
|
|||
impl Default for GroupInternal {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
recently_seen_notif_statuses: VecDeque::new()
|
||||
recently_seen_notif_statuses: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! grp_debug {
|
||||
($self:ident, $f:expr) => {
|
||||
::log::debug!(concat!("(@{}) ", $f), $self.config.get_acct());
|
||||
|
@ -56,6 +57,7 @@ macro_rules! grp_debug {
|
|||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! grp_info {
|
||||
($self:ident, $f:expr) => {
|
||||
::log::info!(concat!("(@{}) ", $f), $self.config.get_acct());
|
||||
|
@ -65,6 +67,7 @@ macro_rules! grp_info {
|
|||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! grp_trace {
|
||||
($self:ident, $f:expr) => {
|
||||
::log::trace!(concat!("(@{}) ", $f), $self.config.get_acct());
|
||||
|
@ -74,6 +77,7 @@ macro_rules! grp_trace {
|
|||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! grp_warn {
|
||||
($self:ident, $f:expr) => {
|
||||
::log::warn!(concat!("(@{}) ", $f), $self.config.get_acct());
|
||||
|
@ -83,6 +87,7 @@ macro_rules! grp_warn {
|
|||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! grp_error {
|
||||
($self:ident, $f:expr) => {
|
||||
::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");
|
||||
|
||||
crate::tr!(self, "mention_prefix", user = notif_acct)
|
||||
+ &crate::tr!(self, "welcome_public")
|
||||
crate::tr!(self, "mention_prefix", user = notif_acct) + &crate::tr!(self, "welcome_public")
|
||||
};
|
||||
|
||||
let post = StatusBuilder::new()
|
||||
|
|
|
@ -18,6 +18,7 @@ use crate::utils::acct_to_server;
|
|||
|
||||
mod command;
|
||||
mod error;
|
||||
#[macro_use]
|
||||
mod group_handler;
|
||||
mod store;
|
||||
mod utils;
|
||||
|
@ -75,10 +76,8 @@ async fn main() -> anyhow::Result<()> {
|
|||
|
||||
let default_level = 3;
|
||||
|
||||
let level = (
|
||||
default_level as isize
|
||||
+ args.occurrences_of("verbose") as isize
|
||||
- args.occurrences_of("quiet") as isize)
|
||||
let level = (default_level as isize + args.occurrences_of("verbose") as isize
|
||||
- args.occurrences_of("quiet") as isize)
|
||||
.clamp(0, LEVELS.len() as isize) as usize;
|
||||
|
||||
env_logger::Builder::new()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::collections::HashMap;
|
||||
use crate::store::DEFAULT_LOCALE_NAME;
|
||||
use crate::tr::TranslationTable;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
|
@ -29,7 +29,7 @@ pub struct CommonConfig {
|
|||
/// This is a work-around for servers that stop sending notifs after a while.
|
||||
pub socket_retire_time_s: f64,
|
||||
#[serde(skip)]
|
||||
pub tr : HashMap<String, TranslationTable>,
|
||||
pub tr: HashMap<String, TranslationTable>,
|
||||
}
|
||||
|
||||
impl Default for CommonConfig {
|
||||
|
@ -52,10 +52,10 @@ impl Default for CommonConfig {
|
|||
}
|
||||
|
||||
impl CommonConfig {
|
||||
pub fn tr(&self, lang : &str) -> &TranslationTable {
|
||||
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")
|
||||
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 crate::error::GroupError;
|
||||
use crate::store::{DEFAULT_LOCALE_NAME, CommonConfig};
|
||||
use crate::store::{CommonConfig, DEFAULT_LOCALE_NAME};
|
||||
use crate::tr::TranslationTable;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
@ -206,7 +206,7 @@ async fn load_locale_override_file(locale_path: impl AsRef<Path>) -> Result<Opti
|
|||
let locale_path = locale_path.as_ref();
|
||||
if locale_path.is_file() {
|
||||
let f = tokio::fs::read(&locale_path).await?;
|
||||
let opt : TranslationTable = json5::from_str(&String::from_utf8_lossy(&f))?;
|
||||
let opt: TranslationTable = json5::from_str(&String::from_utf8_lossy(&f))?;
|
||||
Ok(Some(opt))
|
||||
} else {
|
||||
Ok(None)
|
||||
|
@ -259,7 +259,11 @@ impl GroupConfig {
|
|||
}
|
||||
|
||||
/// (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() {
|
||||
debug!("Creating group directory");
|
||||
tokio::fs::create_dir_all(&group_dir).await?;
|
||||
|
@ -306,12 +310,17 @@ impl GroupConfig {
|
|||
/* state */
|
||||
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();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn from_dir(group_dir: PathBuf, cc : &CommonConfig) -> Result<Self, GroupError> {
|
||||
pub(crate) async fn from_dir(group_dir: PathBuf, cc: &CommonConfig) -> Result<Self, GroupError> {
|
||||
let config_path = group_dir.join("config.json");
|
||||
let control_path = group_dir.join("control.json");
|
||||
let state_path = group_dir.join("state.json");
|
||||
|
@ -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();
|
||||
Ok(g)
|
||||
}
|
||||
|
@ -458,7 +472,7 @@ impl GroupConfig {
|
|||
/// Check if the user's server is banned
|
||||
fn is_users_server_banned(&self, acct: &str) -> bool {
|
||||
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 {
|
||||
|
@ -574,8 +588,8 @@ impl GroupConfig {
|
|||
}
|
||||
}
|
||||
|
||||
fn acct_to_server(acct: &str) -> &str {
|
||||
acct.split('@').nth(1).unwrap_or_default()
|
||||
fn acct_to_server(acct: &str) -> String {
|
||||
crate::utils::acct_to_server(acct).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -588,15 +602,15 @@ mod tests {
|
|||
config: Default::default(),
|
||||
control: Default::default(),
|
||||
state: Default::default(),
|
||||
_group_tr: Default::default()
|
||||
_group_tr: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_acct_to_server() {
|
||||
assert_eq!("pikachu.rocks", acct_to_server("raichu@pikachu.rocks"));
|
||||
assert_eq!("pikachu.rocks", acct_to_server("m@pikachu.rocks"));
|
||||
assert_eq!("", acct_to_server("what"));
|
||||
assert_eq!("pikachu.rocks".to_string(), acct_to_server("raichu@pikachu.rocks"));
|
||||
assert_eq!("pikachu.rocks".to_string(), acct_to_server("m@pikachu.rocks"));
|
||||
assert_eq!("".to_string(), acct_to_server("what"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -666,13 +680,15 @@ mod tests {
|
|||
assert!(!group.is_banned("piggo@piggo.space"), "user not banned by default");
|
||||
|
||||
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.can_write("piggo@piggo.space"), "banned member can't post");
|
||||
|
||||
// unban
|
||||
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");
|
||||
}
|
||||
|
||||
|
|
|
@ -9,9 +9,9 @@ use crate::group_handler::{GroupHandle, GroupInternal};
|
|||
|
||||
pub mod common_config;
|
||||
pub mod group_config;
|
||||
use crate::tr::TranslationTable;
|
||||
pub use common_config::CommonConfig;
|
||||
pub use group_config::GroupConfig;
|
||||
use crate::tr::TranslationTable;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ConfigStore {
|
||||
|
@ -32,8 +32,8 @@ pub struct StoreOptions {
|
|||
pub store_dir: String,
|
||||
}
|
||||
|
||||
const DEFAULT_LOCALE_NAME : &str = "en";
|
||||
const DEFAULT_LOCALE_JSON : &str = include_str!("../../locales/en.json");
|
||||
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.
|
||||
|
@ -214,7 +214,7 @@ impl ConfigStore {
|
|||
Ok(f) => {
|
||||
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);
|
||||
}
|
||||
|
@ -234,10 +234,12 @@ impl ConfigStore {
|
|||
for (k, v) in def_tr.entries() {
|
||||
if !tr.translation_exists(k) {
|
||||
if self.config.validate_locales {
|
||||
warn!("locale \"{}\" is missing \"{}\", default: {:?}",
|
||||
warn!(
|
||||
"locale \"{}\" is missing \"{}\", default: {:?}",
|
||||
locale_name,
|
||||
k,
|
||||
def_tr.get_translation_raw(k).unwrap());
|
||||
def_tr.get_translation_raw(k).unwrap()
|
||||
);
|
||||
}
|
||||
tr.add_translation(k, v);
|
||||
}
|
||||
|
|
17
src/tr.rs
17
src/tr.rs
|
@ -2,7 +2,7 @@
|
|||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug,Clone,Serialize,Deserialize,Default)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct TranslationTable {
|
||||
#[serde(flatten)]
|
||||
entries: HashMap<String, String>,
|
||||
|
@ -15,24 +15,24 @@ impl TranslationTable {
|
|||
}
|
||||
|
||||
/// Iterate all entries
|
||||
pub fn entries(&self) -> impl Iterator<Item=(&String, &String)> {
|
||||
pub fn entries(&self) -> impl Iterator<Item = (&String, &String)> {
|
||||
self.entries.iter()
|
||||
}
|
||||
|
||||
pub fn get_translation_raw(&self, key : &str) -> Option<&str> {
|
||||
pub fn get_translation_raw(&self, key: &str) -> Option<&str> {
|
||||
self.entries.get(key).map(|s| s.as_str())
|
||||
}
|
||||
|
||||
/// Add or update a translation
|
||||
pub fn add_translation(&mut self, key : impl ToString, subs : impl ToString) {
|
||||
pub fn add_translation(&mut self, key: impl ToString, subs: impl ToString) {
|
||||
self.entries.insert(key.to_string(), subs.to_string());
|
||||
}
|
||||
|
||||
pub fn translation_exists(&self, key : &str) -> bool {
|
||||
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 {
|
||||
match self.entries.get(key) {
|
||||
Some(s) => {
|
||||
// TODO optimize
|
||||
|
@ -45,7 +45,7 @@ impl TranslationTable {
|
|||
}
|
||||
s
|
||||
}
|
||||
None => key.to_owned()
|
||||
None => key.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,12 +56,11 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn deser_tr_table() {
|
||||
let tr : TranslationTable = serde_json::from_str(r#"{"foo":"bar"}"#).unwrap();
|
||||
let tr: TranslationTable = serde_json::from_str(r#"{"foo":"bar"}"#).unwrap();
|
||||
assert_eq!("bar", tr.subs("foo", &[]));
|
||||
assert_eq!("xxx", tr.subs("xxx", &[]));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn subs() {
|
||||
let mut tr = TranslationTable::new();
|
||||
|
|
12
src/utils.rs
12
src/utils.rs
|
@ -19,8 +19,8 @@ impl<V, E: Error> LogError for Result<V, E> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn acct_to_server(acct: &str) -> Option<&str> {
|
||||
acct.trim_start_matches('@').split('@').nth(1)
|
||||
pub(crate) fn acct_to_server(acct: &str) -> Option<String> {
|
||||
acct.trim_start_matches('@').split('@').nth(1).map(|s| s.to_lowercase())
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_acct(acct: &str, group: &str) -> Result<String, GroupError> {
|
||||
|
@ -45,8 +45,8 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn test_acct_to_server() {
|
||||
assert_eq!(Some("novak"), acct_to_server("pepa@novak"));
|
||||
assert_eq!(Some("banana.co.uk"), acct_to_server("@pepa@banana.co.uk"));
|
||||
assert_eq!(Some("novak".to_string()), acct_to_server("pepa@novak"));
|
||||
assert_eq!(Some("banana.co.uk".to_string()), acct_to_server("@pepa@banana.co.uk"));
|
||||
assert_eq!(None, acct_to_server("probably_local"));
|
||||
}
|
||||
|
||||
|
@ -82,11 +82,11 @@ mod test {
|
|||
);
|
||||
assert_eq!(
|
||||
Ok("piggo@piggo.space".into()),
|
||||
normalize_acct("piGGgo@pIggo.spaCe", "uhh")
|
||||
normalize_acct("piGGo@pIggo.spaCe", "uhh")
|
||||
);
|
||||
assert_eq!(
|
||||
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"));
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue