use tdlib_rs::enums::MessageContent;
use crate::messages::{Sender, duration};
use crate::text;
#[derive(Clone, Debug, PartialEq)]
pub enum Service {
Did(String),
Quoting(String, String),
Added(Vec<i64>),
Removed(i64),
Note(String),
}
const MAX_NAMES: usize = 3;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Part {
Words(String),
Name(String),
You,
Quote(String),
}
impl Part {
pub fn text(&self) -> &str {
match self {
Part::Words(text) | Part::Name(text) | Part::Quote(text) => text,
Part::You => "You",
}
}
}
pub fn text(parts: &[Part]) -> String {
parts.iter().map(Part::text).collect()
}
impl Service {
pub fn of(content: &MessageContent) -> Option<Self> {
use MessageContent as C;
let did = |words: &str| Some(Service::Did(words.into()));
let quoting = |words: &str, title: &str| {
let title = text::clean(title).replace(['\n', '\t'], " ");
let title = format!("\"{}\"", text::first_chars(&title, 128));
Some(Service::Quoting(words.into(), title))
};
match content {
C::MessageChatAddMembers(m) => Some(Service::Added(m.member_user_ids.clone())),
C::MessageChatDeleteMember(m) => Some(Service::Removed(m.user_id)),
C::MessageChatJoinByLink => did("joined via an invite link"),
C::MessageChatJoinByRequest => did("joined, approved by an admin"),
C::MessageBasicGroupChatCreate(m) => quoting("created the group", &m.title),
C::MessageSupergroupChatCreate(m) => quoting("created", &m.title),
C::MessageChatChangeTitle(m) => quoting("changed the name to", &m.title),
C::MessageChatChangePhoto(_) => did("changed the photo"),
C::MessageChatDeletePhoto => did("removed the photo"),
C::MessageChatUpgradeTo(_) | C::MessageChatUpgradeFrom(_) => {
did("upgraded the group to a supergroup")
}
C::MessagePinMessage(_) => did("pinned a message"),
C::MessageScreenshotTaken => did("took a screenshot"),
C::MessageChatSetMessageAutoDeleteTime(m) => match m.message_auto_delete_time {
0 => did("turned the timer off"),
secs => did(&format!(
"set messages to disappear after {}",
crate::secret::timer_words(secs)
)),
},
C::MessageVideoChatScheduled(_) => did("scheduled a video chat"),
C::MessageVideoChatStarted(_) => did("started a video chat"),
C::MessageVideoChatEnded(m) => {
did(&format!("ended the video chat ({})", duration(m.duration)))
}
C::MessageInviteVideoChatParticipants(_) => did("invited people to the video chat"),
C::MessageChatSetTheme(_) => did("changed the chat theme"),
C::MessageChatSetBackground(_) => did("changed the chat background"),
C::MessageChatBoost(m) if m.boost_count > 1 => {
did(&format!("boosted the group {} times", m.boost_count))
}
C::MessageChatBoost(_) => did("boosted the group"),
C::MessageForumTopicCreated(m) => quoting("created the topic", &m.name),
C::MessageForumTopicEdited(_) => did("edited a topic"),
C::MessageForumTopicIsClosedToggled(m) if m.is_closed => did("closed a topic"),
C::MessageForumTopicIsClosedToggled(_) => did("reopened a topic"),
C::MessageForumTopicIsHiddenToggled(m) if m.is_hidden => did("hid the General topic"),
C::MessageForumTopicIsHiddenToggled(_) => did("showed the General topic"),
C::MessageGameScore(m) => did(&format!("scored {} in a game", m.score)),
C::MessageContactRegistered => did("joined Telegram"),
C::MessageCustomServiceAction(m) => {
let words = text::clean(&m.text).replace(['\n', '\t'], " ");
Some(Service::Note(text::first_chars(&words, 300).to_string()))
}
_ => None,
}
}
pub fn label(&self, sender: Sender) -> String {
let words = match self {
Service::Added(ids) if is_only(ids, sender) => "joined the group".to_string(),
Service::Added(ids) if ids.len() == 1 => "added a member".to_string(),
Service::Added(ids) => format!("added {} members", ids.len()),
Service::Removed(id) if is_only(&[*id], sender) => "left the group".to_string(),
Service::Removed(_) => "removed a member".to_string(),
Service::Did(words) | Service::Note(words) => words.clone(),
Service::Quoting(words, title) => format!("{words} {title}"),
};
let mut chars = words.chars();
chars.next().map_or(String::new(), |first| {
first.to_uppercase().chain(chars).collect()
})
}
pub fn sentence(&self, sender: Sender, actor: Part, name: impl Fn(i64) -> String) -> Vec<Part> {
let words = |text: &str| Part::Words(text.into());
match self {
Service::Added(ids) if is_only(ids, sender) => vec![actor, words(" joined the group")],
Service::Added(ids) => {
let mut parts = vec![actor, words(" added ")];
parts.extend(names(ids, name));
parts
}
Service::Removed(id) if is_only(&[*id], sender) => {
vec![actor, words(" left the group")]
}
Service::Removed(id) => vec![actor, words(" removed "), Part::Name(name(*id))],
Service::Did(did) => vec![actor, words(&format!(" {did}"))],
Service::Quoting(did, title) => vec![
actor,
words(&format!(" {did} ")),
Part::Quote(title.clone()),
],
Service::Note(note) => vec![words(note)],
}
}
}
fn is_only(ids: &[i64], sender: Sender) -> bool {
matches!((ids, sender), ([id], Sender::User(user)) if *id == user)
}
fn names(ids: &[i64], name: impl Fn(i64) -> String) -> Vec<Part> {
let shown: Vec<Part> = ids
.iter()
.take(MAX_NAMES)
.map(|&id| Part::Name(name(id)))
.collect();
let more = ids.len().saturating_sub(MAX_NAMES);
let others = match more {
0 => None,
1 => Some("1 other".to_string()),
n => Some(format!("{n} others")),
};
let count = shown.len() + usize::from(others.is_some());
if count == 0 {
return vec![Part::Words("nobody".into())];
}
let mut parts = Vec::new();
let items = shown.into_iter().chain(others.map(Part::Words)).enumerate();
for (i, item) in items {
if i > 0 {
let joint = if i + 1 == count { " and " } else { ", " };
parts.push(Part::Words(joint.into()));
}
parts.push(item);
}
parts
}
#[cfg(test)]
mod tests {
use tdlib_rs::types;
use super::*;
fn name(id: i64) -> String {
["", "Alice", "Bob", "Carol", "Dan", "Eve"][id as usize].into()
}
fn sentence(content: MessageContent, sender: i64) -> String {
let service = Service::of(&content).expect("a service message");
let sender = Sender::User(sender);
text(&service.sentence(sender, Part::Name(name(1)), name))
}
fn added(ids: &[i64]) -> MessageContent {
MessageContent::MessageChatAddMembers(types::MessageChatAddMembers {
member_user_ids: ids.to_vec(),
})
}
#[test]
fn people_joining_being_added_and_leaving_read_as_sentences() {
assert_eq!(sentence(added(&[1]), 1), "Alice joined the group");
assert_eq!(sentence(added(&[2]), 1), "Alice added Bob");
assert_eq!(sentence(added(&[2, 3]), 1), "Alice added Bob and Carol");
assert_eq!(
sentence(added(&[2, 3, 4, 5]), 1),
"Alice added Bob, Carol, Dan and 1 other"
);
let removed = |user_id| {
MessageContent::MessageChatDeleteMember(types::MessageChatDeleteMember { user_id })
};
assert_eq!(sentence(removed(1), 1), "Alice left the group");
assert_eq!(sentence(removed(2), 1), "Alice removed Bob");
assert_eq!(
sentence(MessageContent::MessageChatJoinByLink, 1),
"Alice joined via an invite link"
);
}
#[test]
fn the_chat_list_says_what_happened_without_names() {
let label = |content, sender| Service::of(&content).unwrap().label(Sender::User(sender));
assert_eq!(label(added(&[1]), 1), "Joined the group");
assert_eq!(label(added(&[2, 3]), 1), "Added 2 members");
assert_eq!(
label(MessageContent::MessageScreenshotTaken, 1),
"Took a screenshot"
);
}
#[test]
fn a_new_name_is_cleaned_and_kept_on_one_line() {
let title = MessageContent::MessageChatChangeTitle(types::MessageChatChangeTitle {
title: "Trip\u{202E}\nplans".into(),
});
assert_eq!(
sentence(title, 1),
"Alice changed the name to \"Trip plans\""
);
}
#[test]
fn names_and_titles_are_parts_of_their_own_apart_from_the_words() {
let service = Service::of(&added(&[2, 3])).unwrap();
let parts = service.sentence(Sender::User(1), Part::You, name);
assert_eq!(
parts,
[
Part::You,
Part::Words(" added ".into()),
Part::Name("Bob".into()),
Part::Words(" and ".into()),
Part::Name("Carol".into()),
]
);
let topic = MessageContent::MessageForumTopicCreated(types::MessageForumTopicCreated {
name: "Thu 8 Oct".into(),
..Default::default()
});
let parts =
Service::of(&topic)
.unwrap()
.sentence(Sender::User(2), Part::Name("Bob".into()), name);
assert_eq!(parts.last(), Some(&Part::Quote("\"Thu 8 Oct\"".into())));
assert_eq!(text(&parts), "Bob created the topic \"Thu 8 Oct\"");
}
#[test]
fn messages_that_arent_about_the_chat_are_not_service_messages() {
assert_eq!(
Service::of(&MessageContent::MessageContact(Default::default())),
None
);
assert_eq!(Service::of(&MessageContent::MessageExpiredPhoto), None);
}
}