Initial commit
This commit is contained in:
commit
84b17515a6
3 changed files with 2055 additions and 0 deletions
1839
Cargo.lock
generated
Normal file
1839
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
12
Cargo.toml
Normal file
12
Cargo.toml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "sticker-usage-analyzer"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
rocket = "0.5"
|
||||||
|
maud = { version = "0.26", features = ["rocket"] }
|
||||||
|
chrono = "0.4"
|
||||||
|
rayon = "1.10"
|
204
src/main.rs
Normal file
204
src/main.rs
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
use maud::{html, Markup, DOCTYPE};
|
||||||
|
use rocket::{Rocket,Build,launch,get,routes,fs::{FileServer}};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use rayon::prelude::*;
|
||||||
|
|
||||||
|
|
||||||
|
#[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: i64,
|
||||||
|
pub times: u32
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ChatMessage> for ScoredChatMessage {
|
||||||
|
fn from(item: ChatMessage) -> ScoredChatMessage {
|
||||||
|
ScoredChatMessage {
|
||||||
|
chat_message: item.clone(),
|
||||||
|
last_used: item.date_unixtime.parse().expect("Could not parse date_unixtime"),
|
||||||
|
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 = DateTime::from_timestamp(item.last_used, 0).expect("Could not parse items");
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[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 {
|
||||||
|
println!("Parsing JSON... (this can take a moment)");
|
||||||
|
let tg_export_result: Chat = serde_json::from_reader(
|
||||||
|
std::fs::OpenOptions::new().read(true).open("./result.json").expect("Could not open ./result.json")
|
||||||
|
).expect("Could not parse result.json");
|
||||||
|
println!("Done!");
|
||||||
|
|
||||||
|
let messages: Vec<ChatMessage> = tg_export_result.messages.into_par_iter().filter(|m| {
|
||||||
|
return if let Some(media_type) = &m.media_type {
|
||||||
|
*media_type == MediaType::Sticker
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
let mut messages: Vec<ScoredChatMessage> = messages.into_iter().fold(HashMap::new(), |mut acc: HashMap<String, ScoredChatMessage>, message| {
|
||||||
|
let file = message.file.as_ref().expect("No file").to_owned();
|
||||||
|
if acc.contains_key(&file) {
|
||||||
|
if let Some(scored_chat_message) = acc.get_mut(&file) {
|
||||||
|
let message_date = message.date_unixtime.parse().expect("Could not parse date_unixtime");
|
||||||
|
if scored_chat_message.last_used < message_date {
|
||||||
|
scored_chat_message.last_used = message_date;
|
||||||
|
}
|
||||||
|
scored_chat_message.times = scored_chat_message.times + 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
acc.insert(file, ScoredChatMessage::from(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in a new issue