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