use maud::{html, Markup, DOCTYPE}; use rocket::{Rocket,Build,launch,get,routes,fs::{FileServer}}; use std::{fs::OpenOptions, io::BufReader, collections::HashMap}; use chrono::prelude::*; const TELEGRAM_TIMESTAMP_FORMAT: &str = "%FT%X"; #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "snake_case")] enum MediaType { Sticker, Animation, VideoFile, AudioFile, VoiceMessage, VideoMessage } #[derive(Debug, Clone)] struct ScoredChatMessage { pub chat_message: ChatMessage, pub last_used: NaiveDateTime, pub times: u32 } impl TryFrom for ScoredChatMessage { type Error = chrono::format::ParseError; fn try_from(value: ChatMessage) -> Result { Ok(ScoredChatMessage { chat_message: value.clone(), last_used: NaiveDateTime::parse_from_str(&value.date, TELEGRAM_TIMESTAMP_FORMAT)?, times: 1 }) } } impl From for Markup { fn from(item: ScoredChatMessage) -> Markup { if let Some(mime_type) = &item.chat_message.mime_type { let message_file = item.chat_message.file.expect("No sticker file"); let message_date = item.last_used; match mime_type.as_str() { "image/webp" => { return html! { .sticker { img loading="lazy" src=(message_file); p { "Times used: " (item.times) br; (message_date.format("%Y-%m-%d %H:%M:%S")) } } }; } "video/webm" => { return html! { .sticker.animated { video loading="lazy" autoplay loop controls src=(message_file) {} p { "Times used: " (item.times) br; (message_date.format("%Y-%m-%d %H:%M:%S")) } } } } "application/x-tgsticker" => { return html! { .sticker.animated { tgs-player loading="lazy" autoplay loop controls src=(message_file) {} p { "Times used: " (item.times) br; (message_date.format("%Y-%m-%d %H:%M:%S")) } } } } _ => { return html! { p { "Unknown type " (mime_type) } }; } } } else { return html! { p { "No type" } }; } } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] struct ChatMessage { pub media_type: Option, pub mime_type: Option, pub file: Option, pub date_unixtime: String, pub date: String } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] struct Chat { name: String, messages: Vec } #[launch] fn rocket() -> Rocket { rocket::build() .mount("/", routes![index]) .mount("/stickers", FileServer::from("./stickers")) .mount("/video_files", FileServer::from("./video_files")) } #[get("/")] fn index() -> Markup { let tg_export_result: Chat = serde_json::from_reader( BufReader::new(OpenOptions::new().read(true).open("./result.json").expect("Could not open ./result.json")) ).expect("Could not parse result.json"); let mut messages: Vec = tg_export_result.messages.into_iter() .filter(|m| matches!(m.media_type, Some(MediaType::Sticker))) .fold(HashMap::new(), |mut acc: HashMap, message| { if let Some(ref file) = message.file { let message_date = NaiveDateTime::parse_from_str(&message.date, TELEGRAM_TIMESTAMP_FORMAT) .expect("Could not parse date"); let entry = acc.entry(file.to_owned()).or_insert_with(|| ScoredChatMessage::try_from(message) .expect("Could not parse date")); entry.last_used = entry.last_used.max(message_date); entry.times += 1; } acc }) .into_values() .collect(); messages.sort_by(|a, b| b.times.cmp(&a.times)); return html! { (DOCTYPE) head { script src="https://unpkg.com/@lottiefiles/lottie-player@0.4.0/dist/tgs-player.js" {} } body { style { r#"* { box-sizing: border-box; } h1 {text-align: center;} body { width: 100vw; margin: 0; padding: 0; } .container { max-width: 1024px; margin: 0 auto; } .container, .stickers, .sticker { display: flex; position: relative; flex-flow: row wrap; } .sticker { flex: 0 0 25%; align-items: flex-end; padding: 1rem; border: 1px solid #00000055; } .sticker p { flex: 1; text-align: center; } img, video, tgs-player { width: 100%; height: auto; margin: 0 auto; aspect-ratio: 1 / 1; object-fit: contain; }"# } .container { h1 { "Stickers with " (tg_export_result.name) } .stickers { @for message in messages { (>::into(message)) } } } } }; }