wip smart split to fit instance character limit

master
Ondřej Hruška 3 years ago
parent e5ce0bdeb7
commit 748023c410
  1. 143
      src/group_handler/handle_mention.rs
  2. 7
      src/store/data.rs
  3. 16
      src/store/mod.rs

@ -262,8 +262,30 @@ impl<'a> ProcessMention<'a> {
apply_trailing_hashtag_pleroma_bug_workaround(&mut msg); apply_trailing_hashtag_pleroma_bug_workaround(&mut msg);
} }
let mention = format!("@{user}", user = self.status_acct);
self.send_reply_multipart(mention, msg).await?;
}
if !self.announcements.is_empty() {
let mut msg = self.announcements.join("\n");
debug!("a={}", msg);
if self.want_markdown {
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() if let Ok(post) = StatusBuilder::new()
.status(format!("@{user} {msg}", user = self.status_acct, msg = msg)) .status(p)
.content_type(if self.want_markdown { .content_type(if self.want_markdown {
"text/markdown" "text/markdown"
} else { } else {
@ -272,30 +294,31 @@ impl<'a> ProcessMention<'a> {
.visibility(Visibility::Direct) .visibility(Visibility::Direct)
.build() .build()
{ {
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;
} }
// Sleep a bit to avoid throttling
tokio::time::sleep(Duration::from_secs(1)).await;
} }
if !self.announcements.is_empty() { Ok(())
let mut msg = self.announcements.join("\n"); }
debug!("a={}", msg);
if self.want_markdown { async fn send_announcement_multipart(&self, msg : &str) -> Result<(), GroupError> {
apply_trailing_hashtag_pleroma_bug_workaround(&mut msg); 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,95 @@ 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") {
if this_piece.len() + l.len() == 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 {
let trimmed = this_piece.trim();
if !trimmed.is_empty() {
parts_to_send.push(trimmed.to_owned());
}
// start new piece
this_piece = format!("{}{}", prefix, l);
while this_piece.len() > limit {
let to_send = if let Some(last_space) = (&this_piece[..limit]).rfind(' ') {
let mut p = this_piece.split_off(last_space);
std::mem::swap(&mut p, &mut this_piece);
p
} else {
let mut p = this_piece.split_off(limit);
std::mem::swap(&mut p, &mut this_piece);
p
};
parts_to_send.push(to_send);
this_piece = format!("{}{}", prefix, this_piece);
}
this_piece.push('\n');
} else {
this_piece.push_str(l);
this_piece.push('\n');
}
}
parts_to_send
}
#[cfg(test)]
mod test {
#[test]
fn test_smart_split1() {
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_split2() {
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_split3() {
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_split4() {
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);
}
}

@ -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
} }

@ -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)
// }
// }
// }

Loading…
Cancel
Save