sticker-usage-analyzer-rs/src/main.rs

194 lines
6.1 KiB
Rust

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<ChatMessage> for ScoredChatMessage {
type Error = chrono::format::ParseError;
fn try_from(value: ChatMessage) -> Result<Self, Self::Error> {
Ok(ScoredChatMessage {
chat_message: value.clone(),
last_used: NaiveDateTime::parse_from_str(&value.date, TELEGRAM_TIMESTAMP_FORMAT)?,
times: 1
})
}
}
impl From<ScoredChatMessage> 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<MediaType>,
pub mime_type: Option<String>,
pub file: Option<String>,
pub date_unixtime: String,
pub date: String
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct Chat {
name: String,
messages: Vec<ChatMessage>
}
#[launch]
fn rocket() -> Rocket<Build> {
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<ScoredChatMessage> = tg_export_result.messages.into_iter()
.filter(|m| matches!(m.media_type, Some(MediaType::Sticker)))
.fold(HashMap::new(), |mut acc: HashMap<String, ScoredChatMessage>, 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 {
(<ScoredChatMessage as Into<Markup>>::into(message))
}
}
}
}
};
}