Merge branch 'smart-split'

This commit is contained in:
Ondřej Hruška 2021-09-19 14:52:37 +02:00
commit 6c9041eedd
3 changed files with 238 additions and 34 deletions

View file

@ -262,21 +262,8 @@ impl<'a> ProcessMention<'a> {
apply_trailing_hashtag_pleroma_bug_workaround(&mut msg); apply_trailing_hashtag_pleroma_bug_workaround(&mut msg);
} }
if let Ok(post) = StatusBuilder::new() let mention = format!("@{user} ", user = self.status_acct);
.status(format!("@{user} {msg}", user = self.status_acct, msg = msg)) self.send_reply_multipart(mention, msg).await?;
.content_type(if self.want_markdown {
"text/markdown"
} else {
"text/plain"
})
.visibility(Visibility::Direct)
.build()
{
let _ = self.client.new_status(post)
.await.log_error("Failed to post");
// Sleep a bit to avoid throttling
tokio::time::sleep(Duration::from_secs(1)).await;
}
} }
if !self.announcements.is_empty() { if !self.announcements.is_empty() {
@ -287,15 +274,51 @@ impl<'a> ProcessMention<'a> {
apply_trailing_hashtag_pleroma_bug_workaround(&mut msg); apply_trailing_hashtag_pleroma_bug_workaround(&mut msg);
} }
let msg = format!("**📢 Group announcement**\n{msg}", msg = msg);
self.send_announcement_multipart(&msg).await?;
}
Ok(())
}
async fn send_reply_multipart(&self, mention : String, msg : String) -> Result<(), GroupError> {
let parts = smart_split(&msg, Some(mention), self.config.get_character_limit());
for p in parts {
if let Ok(post) = StatusBuilder::new()
.status(p)
.content_type(if self.want_markdown {
"text/markdown"
} else {
"text/plain"
})
.visibility(Visibility::Direct)
.build()
{
self.client.new_status(post).await?;
}
// Sleep a bit to avoid throttling
tokio::time::sleep(Duration::from_secs(1)).await;
}
Ok(())
}
async fn send_announcement_multipart(&self, msg : &str) -> Result<(), GroupError> {
let parts = smart_split(msg, None, self.config.get_character_limit());
for p in parts {
let post = StatusBuilder::new() let post = StatusBuilder::new()
.status(format!("**📢 Group announcement**\n{msg}", msg = msg)) .status(p)
.content_type("text/markdown") .content_type("text/markdown")
.visibility(Visibility::Public) .visibility(Visibility::Public)
.build() .build()
.expect("error build status"); .expect("error build status");
let _ = self.client.new_status(post) self.client.new_status(post).await?;
.await.log_error("Failed to post");
// Sleep a bit to avoid throttling
tokio::time::sleep(Duration::from_secs(1)).await;
} }
Ok(()) Ok(())
@ -772,3 +795,193 @@ fn apply_trailing_hashtag_pleroma_bug_workaround(msg: &mut String) {
msg.push_str(" ."); msg.push_str(" .");
} }
} }
fn smart_split(msg : &str, prefix: Option<String>, limit: usize) -> Vec<String> {
let prefix = prefix.unwrap_or_default();
if msg.len() + prefix.len() < limit {
return vec![format!("{}{}", prefix, msg)];
}
let mut parts_to_send = vec![];
let mut this_piece = prefix.clone();
for l in msg.split("\n") {
println!("* Line: {:?}", l);
if this_piece.len() + l.len() == 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);
} else if this_piece.len() + l.len() > limit {
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);
parts_to_send.push(trimmed.to_owned());
}
}
// start new piece with the line. If the line is too long, break it up.
this_piece = format!("{}{}", prefix, l);
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");
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)");
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);
parts_to_send.push(part_trimmed.to_owned());
this_piece = format!("{}{}", prefix, this_piece.trim());
}
this_piece.push('\n');
} else {
println!("append line");
// this line still fits comfortably
this_piece.push_str(l);
this_piece.push('\n');
}
}
if this_piece != prefix {
let leftover_trimmed = this_piece.trim();
if !leftover_trimmed.is_empty() {
println!("flush buffer: {:?}", leftover_trimmed);
parts_to_send.push(leftover_trimmed.to_owned());
}
}
parts_to_send
}
#[cfg(test)]
mod test {
#[test]
fn test_smart_split_lines() {
let to_split = "a234567890\nb234567890\nc234567890\nd234\n67890\ne234567890\n";
let parts = super::smart_split(to_split, None, 10);
assert_eq!(vec![
"a234567890".to_string(),
"b234567890".to_string(),
"c234567890".to_string(),
"d234\n67890".to_string(),
"e234567890".to_string(),
], parts);
}
#[test]
fn test_smart_split_nosplit() {
let to_split = "foo\nbar\nbaz";
let parts = super::smart_split(to_split, None, 1000);
assert_eq!(vec![
"foo\nbar\nbaz".to_string(),
], parts);
}
#[test]
fn test_smart_split_nosplit_prefix() {
let to_split = "foo\nbar\nbaz";
let parts = super::smart_split(to_split, Some("PREFIX".to_string()), 1000);
assert_eq!(vec![
"PREFIXfoo\nbar\nbaz".to_string(),
], parts);
}
#[test]
fn test_smart_split_prefix_each() {
let to_split = "1234\n56\n7";
let parts = super::smart_split(to_split, Some("PREFIX".to_string()), 10);
assert_eq!(vec![
"PREFIX1234".to_string(),
"PREFIX56\n7".to_string(),
], parts);
}
#[test]
fn test_smart_split_words() {
let to_split = "one two three four five six seven eight nine ten";
let parts = super::smart_split(to_split, None, 10);
assert_eq!(vec![
"one two".to_string(),
"three four".to_string(),
"five six".to_string(),
"seven".to_string(),
"eight nine".to_string(),
"ten".to_string(),
], parts);
}
#[test]
fn test_smart_split_words_multispace() {
let to_split = "one two three four five six seven eight nine ten ";
let parts = super::smart_split(to_split, None, 10);
assert_eq!(vec![
"one two".to_string(),
"three four".to_string(),
"five six".to_string(),
"seven".to_string(),
"eight nine".to_string(),
"ten".to_string(),
], parts);
}
#[test]
fn test_smart_split_words_longword() {
let to_split = "one two threefourfive six";
let parts = super::smart_split(to_split, None, 10);
assert_eq!(vec![
"one two".to_string(),
"threefourf".to_string(),
"ive six".to_string(),
], parts);
}
#[test]
fn test_smart_split_words_prefix() {
let to_split = "one two three four five six seven eight nine ten";
let parts = super::smart_split(to_split, Some("PREFIX".to_string()), 15);
assert_eq!(vec![
"PREFIXone two".to_string(),
"PREFIXthree".to_string(),
"PREFIXfour five".to_string(),
"PREFIXsix seven".to_string(),
"PREFIXeight".to_string(),
"PREFIXnine ten".to_string(),
], parts);
}
#[test]
fn test_smart_split_realistic() {
let to_split = "\
Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n\
Aenean venenatis libero ac ex suscipit, nec efficitur arcu convallis.\n\
Nulla ante neque, efficitur nec fermentum a, fermentum nec nisl.\n\
Sed dolor ex, vestibulum at malesuada ut, faucibus ac ante.\n\
Nullam scelerisque magna dui, id tempor purus faucibus sit amet.\n\
Curabitur pretium condimentum pharetra.\n\
Aenean dictum, tortor et ultrices fermentum, mauris erat vehicula lectus.\n\
Nec varius mauris sem sollicitudin dolor. Nunc porta in urna nec vulputate.";
let parts = super::smart_split(to_split, Some("@pepa@pig.club ".to_string()), 140);
assert_eq!(vec![
"@pepa@pig.club Lorem ipsum dolor sit amet, consectetur adipiscing elit.".to_string(),
"@pepa@pig.club Aenean venenatis libero ac ex suscipit, nec efficitur arcu convallis.".to_string(),
"@pepa@pig.club Nulla ante neque, efficitur nec fermentum a, fermentum nec nisl.\nSed dolor ex, vestibulum at malesuada ut, faucibus ac ante.".to_string(),
"@pepa@pig.club Nullam scelerisque magna dui, id tempor purus faucibus sit amet.\nCurabitur pretium condimentum pharetra.".to_string(),
"@pepa@pig.club Aenean dictum, tortor et ultrices fermentum, mauris erat vehicula lectus.".to_string(),
"@pepa@pig.club Nec varius mauris sem sollicitudin dolor. Nunc porta in urna nec vulputate.".to_string(),
], parts);
}
}

View file

@ -33,6 +33,8 @@ pub(crate) struct GroupConfig {
acct: String, acct: String,
/// elefren data /// elefren data
appdata: AppData, appdata: AppData,
/// Server's character limit
character_limit: usize,
/// Hashtags the group will auto-boost from it's members /// Hashtags the group will auto-boost from it's members
group_tags: HashSet<String>, group_tags: HashSet<String>,
/// List of admin account "acct" names, e.g. piggo@piggo.space /// List of admin account "acct" names, e.g. piggo@piggo.space
@ -67,6 +69,7 @@ impl Default for GroupConfig {
redirect: Default::default(), redirect: Default::default(),
token: Default::default(), token: Default::default(),
}, },
character_limit: 5000,
group_tags: Default::default(), group_tags: Default::default(),
admin_users: Default::default(), admin_users: Default::default(),
member_users: Default::default(), member_users: Default::default(),
@ -90,6 +93,10 @@ impl GroupConfig {
} }
} }
pub(crate) fn get_character_limit(&self) -> usize {
self.character_limit
}
pub(crate) fn is_enabled(&self) -> bool { pub(crate) fn is_enabled(&self) -> bool {
self.enabled self.enabled
} }

View file

@ -232,19 +232,3 @@ fn make_scopes() -> Scopes {
| Scopes::write(scopes::Write::Media) | Scopes::write(scopes::Write::Media)
| Scopes::write(scopes::Write::Follows) | Scopes::write(scopes::Write::Follows)
} }
// trait TapOk<T> {
// fn tap_ok<F: FnOnce(&T)>(self, f: F) -> Self;
// }
//
// impl<T, E> TapOk<T> for Result<T, E> {
// fn tap_ok<F: FnOnce(&T)>(self, f: F) -> Self {
// match self {
// Ok(v) => {
// f(&v);
// Ok(v)
// }
// Err(e) => Err(e)
// }
// }
// }