use serde::{Deserialize, Serialize};
use std::hash::{Hash, Hasher};
use std::path::PathBuf;
use super::new_fixed_hasher;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ContentType {
Text,
Photo,
Video,
Animation,
Document,
Sticker,
Voice,
VideoNote,
Audio,
Location,
Venue,
Contact,
Poll,
Dice,
}
impl ContentType {
#[must_use]
pub fn can_edit_to(&self, target: &ContentType) -> bool {
use ContentType::{Animation, Document, Photo, Text, Video};
match (self, target) {
(Text, Text) => true,
(Photo | Video | Animation | Document, Photo | Video | Animation | Document) => true,
_ => false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub enum ParseMode {
#[default]
Html,
MarkdownV2,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub enum LinkPreview {
Enabled,
#[default]
Disabled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FileSource {
FileId(String),
Url(String),
LocalPath(PathBuf),
Bytes {
data: Vec<u8>,
filename: String,
},
}
impl PartialEq for FileSource {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::FileId(a), Self::FileId(b)) => a == b,
(Self::Url(a), Self::Url(b)) => a == b,
(Self::LocalPath(a), Self::LocalPath(b)) => a == b,
(
Self::Bytes {
data: d1,
filename: f1,
},
Self::Bytes {
data: d2,
filename: f2,
},
) => d1 == d2 && f1 == f2,
_ => false,
}
}
}
impl Eq for FileSource {}
impl Hash for FileSource {
fn hash<H: Hasher>(&self, state: &mut H) {
match self {
Self::FileId(id) => {
0u8.hash(state);
id.hash(state);
}
Self::Url(url) => {
1u8.hash(state);
url.hash(state);
}
Self::LocalPath(p) => {
2u8.hash(state);
p.hash(state);
}
Self::Bytes { data, filename } => {
3u8.hash(state);
data.hash(state);
filename.hash(state);
}
}
}
}
impl From<&str> for FileSource {
fn from(s: &str) -> Self {
if s.starts_with("http://") || s.starts_with("https://") {
Self::Url(s.to_string())
} else if s.contains('/') || s.contains('\\') {
Self::LocalPath(PathBuf::from(s))
} else {
Self::FileId(s.to_string())
}
}
}
impl From<String> for FileSource {
fn from(s: String) -> Self {
Self::from(s.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MessageContent {
Text {
text: String,
parse_mode: ParseMode,
keyboard: Option<crate::keyboard::InlineKeyboard>,
link_preview: LinkPreview,
},
Photo {
source: FileSource,
caption: Option<String>,
parse_mode: ParseMode,
keyboard: Option<crate::keyboard::InlineKeyboard>,
spoiler: bool,
},
Video {
source: FileSource,
caption: Option<String>,
parse_mode: ParseMode,
keyboard: Option<crate::keyboard::InlineKeyboard>,
spoiler: bool,
},
Animation {
source: FileSource,
caption: Option<String>,
parse_mode: ParseMode,
keyboard: Option<crate::keyboard::InlineKeyboard>,
spoiler: bool,
},
Document {
source: FileSource,
caption: Option<String>,
parse_mode: ParseMode,
keyboard: Option<crate::keyboard::InlineKeyboard>,
filename: Option<String>,
},
Sticker {
source: FileSource,
},
Location {
latitude: f64,
longitude: f64,
keyboard: Option<crate::keyboard::InlineKeyboard>,
},
}
impl MessageContent {
pub fn content_type(&self) -> ContentType {
match self {
Self::Text { .. } => ContentType::Text,
Self::Photo { .. } => ContentType::Photo,
Self::Video { .. } => ContentType::Video,
Self::Animation { .. } => ContentType::Animation,
Self::Document { .. } => ContentType::Document,
Self::Sticker { .. } => ContentType::Sticker,
Self::Location { .. } => ContentType::Location,
}
}
pub fn content_hash(&self) -> u64 {
let mut hasher = new_fixed_hasher();
self.content_type().hash(&mut hasher);
match self {
Self::Text {
text,
parse_mode,
keyboard,
link_preview,
} => {
text.hash(&mut hasher);
parse_mode.hash(&mut hasher);
if let Some(kb) = keyboard {
kb.hash(&mut hasher);
}
link_preview.hash(&mut hasher);
}
Self::Photo {
source,
caption,
keyboard,
spoiler,
parse_mode,
} => {
source.hash(&mut hasher);
caption.hash(&mut hasher);
if let Some(kb) = keyboard {
kb.hash(&mut hasher);
}
spoiler.hash(&mut hasher);
parse_mode.hash(&mut hasher);
}
Self::Video {
source,
caption,
keyboard,
spoiler,
parse_mode,
} => {
source.hash(&mut hasher);
caption.hash(&mut hasher);
if let Some(kb) = keyboard {
kb.hash(&mut hasher);
}
spoiler.hash(&mut hasher);
parse_mode.hash(&mut hasher);
}
Self::Animation {
source,
caption,
keyboard,
spoiler,
parse_mode,
} => {
source.hash(&mut hasher);
caption.hash(&mut hasher);
if let Some(kb) = keyboard {
kb.hash(&mut hasher);
}
spoiler.hash(&mut hasher);
parse_mode.hash(&mut hasher);
}
Self::Document {
source,
caption,
keyboard,
filename,
parse_mode,
} => {
source.hash(&mut hasher);
caption.hash(&mut hasher);
if let Some(kb) = keyboard {
kb.hash(&mut hasher);
}
filename.hash(&mut hasher);
parse_mode.hash(&mut hasher);
}
Self::Sticker { source } => {
source.hash(&mut hasher);
}
Self::Location {
latitude,
longitude,
keyboard,
} => {
latitude.to_bits().hash(&mut hasher);
longitude.to_bits().hash(&mut hasher);
if let Some(kb) = keyboard {
kb.hash(&mut hasher);
}
}
}
hasher.finish()
}
pub fn text_hash(&self) -> u64 {
let mut hasher = new_fixed_hasher();
match self {
Self::Text {
text, parse_mode, ..
} => {
1u8.hash(&mut hasher); text.hash(&mut hasher);
parse_mode.hash(&mut hasher);
}
_ => {
0u8.hash(&mut hasher); }
}
hasher.finish()
}
pub fn caption(&self) -> Option<String> {
match self {
Self::Photo { caption, .. }
| Self::Video { caption, .. }
| Self::Animation { caption, .. }
| Self::Document { caption, .. } => caption.clone(),
_ => None,
}
}
pub fn keyboard(&self) -> Option<crate::keyboard::InlineKeyboard> {
match self {
Self::Text { keyboard, .. }
| Self::Photo { keyboard, .. }
| Self::Video { keyboard, .. }
| Self::Animation { keyboard, .. }
| Self::Document { keyboard, .. }
| Self::Location { keyboard, .. } => keyboard.clone(),
_ => None,
}
}
pub fn keyboard_hash(&self) -> u64 {
let mut hasher = new_fixed_hasher();
match self.keyboard() {
Some(kb) => {
1u8.hash(&mut hasher);
kb.hash(&mut hasher);
}
None => {
0u8.hash(&mut hasher);
}
}
hasher.finish()
}
pub fn as_plain_text(&self) -> Self {
fn strip(html: &str) -> String {
let mut out = String::with_capacity(html.len());
let mut inside_tag = false;
for ch in html.chars() {
match ch {
'<' => inside_tag = true,
'>' if inside_tag => inside_tag = false,
_ if !inside_tag => out.push(ch),
_ => {}
}
}
out.replace("<", "<")
.replace(">", ">")
.replace("&", "&")
.replace(""", "\"")
}
match self.clone() {
Self::Text {
text,
keyboard,
link_preview,
..
} => Self::Text {
text: strip(&text),
parse_mode: ParseMode::None,
keyboard,
link_preview,
},
Self::Photo {
source,
caption,
keyboard,
spoiler,
..
} => Self::Photo {
source,
caption: caption.map(|c| strip(&c)),
parse_mode: ParseMode::None,
keyboard,
spoiler,
},
Self::Video {
source,
caption,
keyboard,
spoiler,
..
} => Self::Video {
source,
caption: caption.map(|c| strip(&c)),
parse_mode: ParseMode::None,
keyboard,
spoiler,
},
Self::Animation {
source,
caption,
keyboard,
spoiler,
..
} => Self::Animation {
source,
caption: caption.map(|c| strip(&c)),
parse_mode: ParseMode::None,
keyboard,
spoiler,
},
Self::Document {
source,
caption,
keyboard,
filename,
..
} => Self::Document {
source,
caption: caption.map(|c| strip(&c)),
parse_mode: ParseMode::None,
keyboard,
filename,
},
other => other, }
}
pub fn caption_hash(&self) -> u64 {
let mut hasher = new_fixed_hasher();
match self.caption() {
Some(cap) => {
1u8.hash(&mut hasher);
cap.hash(&mut hasher);
}
None => {
0u8.hash(&mut hasher);
}
}
hasher.finish()
}
pub fn file_hash(&self) -> u64 {
let mut hasher = new_fixed_hasher();
match self {
Self::Photo { source, .. }
| Self::Video { source, .. }
| Self::Animation { source, .. }
| Self::Document { source, .. }
| Self::Sticker { source } => {
1u8.hash(&mut hasher);
source.hash(&mut hasher);
}
_ => {
0u8.hash(&mut hasher);
}
}
hasher.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn content_hash_differs_on_parse_mode_photo() {
let a = MessageContent::Photo {
source: FileSource::FileId("abc".into()),
caption: Some("cap".into()),
parse_mode: ParseMode::Html,
keyboard: None,
spoiler: false,
};
let b = MessageContent::Photo {
source: FileSource::FileId("abc".into()),
caption: Some("cap".into()),
parse_mode: ParseMode::MarkdownV2,
keyboard: None,
spoiler: false,
};
assert_ne!(
a.content_hash(),
b.content_hash(),
"different parse_mode must produce different hash"
);
}
#[test]
fn content_hash_differs_on_parse_mode_video() {
let a = MessageContent::Video {
source: FileSource::FileId("v".into()),
caption: None,
parse_mode: ParseMode::Html,
keyboard: None,
spoiler: false,
};
let b = MessageContent::Video {
source: FileSource::FileId("v".into()),
caption: None,
parse_mode: ParseMode::None,
keyboard: None,
spoiler: false,
};
assert_ne!(a.content_hash(), b.content_hash());
}
#[test]
fn content_hash_differs_on_parse_mode_animation() {
let a = MessageContent::Animation {
source: FileSource::FileId("g".into()),
caption: None,
parse_mode: ParseMode::Html,
keyboard: None,
spoiler: false,
};
let b = MessageContent::Animation {
source: FileSource::FileId("g".into()),
caption: None,
parse_mode: ParseMode::MarkdownV2,
keyboard: None,
spoiler: false,
};
assert_ne!(a.content_hash(), b.content_hash());
}
#[test]
fn content_hash_differs_on_parse_mode_document() {
let a = MessageContent::Document {
source: FileSource::FileId("d".into()),
caption: None,
parse_mode: ParseMode::Html,
keyboard: None,
filename: None,
};
let b = MessageContent::Document {
source: FileSource::FileId("d".into()),
caption: None,
parse_mode: ParseMode::None,
keyboard: None,
filename: None,
};
assert_ne!(a.content_hash(), b.content_hash());
}
}