1use chrono::prelude::*;
2use lazy_static::lazy_static;
3use regex::Regex;
4use serde::Deserialize;
5use std::fs::File;
6use std::io::Read;
7use std::path::PathBuf;
8use zip::ZipArchive;
9
10lazy_static! {
11 static ref JSON_FILE_NAME: Regex = Regex::new(r".*\/\d{4}-\d{2}-\d{2}.json$").unwrap();
13}
14
15#[derive(Deserialize, Debug)]
17#[allow(dead_code)]
18pub struct UserProfile {
19 avatar_hash: String,
20 image_72: String,
22 first_name: String,
23 real_name: String,
24 display_name: String,
25 team: String,
26 name: String,
27 is_restricted: bool,
28 is_ultra_restricted: bool,
29}
30
31#[derive(Deserialize, Debug)]
33#[allow(dead_code)]
34pub struct Message {
35 user: Option<String>,
36 #[serde(rename = "type")]
37 json_type: String,
38 ts: String,
39 client_msg_id: Option<String>,
40 pub text: String,
41 team: Option<String>,
42 user_team: Option<String>,
43 source_team: Option<String>,
44 user_profile: Option<UserProfile>,
45 thread_ts: Option<String>,
46 parent_user_id: Option<String>,
47 attachments: Option<Vec<MessageAttachment>>,
48 blocks: Option<Vec<MessageBlock>>,
49}
50impl Message {
51 #[cfg(test)]
52 fn new(user: &str, timestamp: &str, text: &str) -> Message {
53 Message {
54 user: Option::Some(user.into()),
55 json_type: "message".into(),
56 ts: timestamp.into(),
57 client_msg_id: Option::None,
58 text: text.into(),
59 team: Option::None,
60 user_team: Option::None,
61 source_team: Option::None,
62 user_profile: Option::None,
63 thread_ts: Option::None,
64 parent_user_id: Option::None,
65 attachments: Option::None,
66 blocks: Option::None,
67 }
68 }
69
70 pub fn time(&self) -> chrono::DateTime<chrono::Utc> {
73 let seconds: i64 = self
74 .ts
75 .split(".")
76 .next()
77 .expect("Invalid timestamp format.")
78 .parse::<i64>()
79 .expect("First part of timestamp is not an integer.");
80 DateTime::from_timestamp(seconds, 0).unwrap()
81 }
82
83 pub fn contains(&self, pattern: &str) -> bool {
85 if self.text.contains(pattern) {
86 return true;
87 }
88 for attachment in self.attachments.iter().flatten() {
89 if attachment.contains(pattern) {
90 return true;
91 }
92 }
93 for block in self.blocks.iter().flatten() {
94 if block.contains(pattern) {
95 return true;
96 }
97 }
98 return false;
99 }
100}
101
102#[derive(Deserialize, Debug)]
104#[allow(dead_code)]
105pub struct MessageAttachment {
106 id: Option<u64>,
107 text: Option<String>,
108}
109impl MessageAttachment {
110 pub fn contains(&self, pattern: &str) -> bool {
112 if let Some(text) = &self.text {
113 return text.contains(pattern);
114 }
115 false
116 }
117}
118
119#[derive(Deserialize, Debug)]
121#[allow(dead_code)]
122pub struct MessageBlock {
123 #[serde(rename = "type")]
124 json_type: String,
125 block_id: Option<String>,
126 text: Option<String>,
127 elements: Option<Vec<MessageBlock>>,
128}
129impl MessageBlock {
130 pub fn contains(&self, pattern: &str) -> bool {
132 if let Some(text) = &self.text {
133 return text.contains(pattern);
134 }
135 if let Some(elements) = &self.elements {
136 for element in elements {
137 if element.contains(pattern) {
138 return true;
139 }
140 }
141 }
142 false
143 }
144}
145
146#[derive(Debug)]
151pub struct MessageInChannel {
152 pub channel: String,
153 pub message: Message,
154}
155impl MessageInChannel {
156 pub fn new(channel: &str, message: Message) -> MessageInChannel {
157 MessageInChannel {
158 channel: channel.into(),
159 message,
160 }
161 }
162}
163
164fn read_file(file_name: &str, file_content: &str) -> Vec<Message> {
165 match serde_json::from_str(file_content) {
166 Ok(x) => x,
167 Err(x) => {
168 eprint!("Could not deserialize '{}': {}.", file_name, x.to_string());
169 Vec::new()
170 }
171 }
172}
173
174pub fn read_zip_contents(zip_path: &PathBuf) -> Vec<MessageInChannel> {
176 let file = File::open(zip_path).expect("Cannot open file");
177 let mut archive: ZipArchive<File> = ZipArchive::new(file).expect("ZIP file invalid.");
178 let mut result: Vec<MessageInChannel> = Vec::new();
179 println!("Number of files in archive: {}", archive.len());
180 let mut counter: u32 = 0;
181
182 for i in 0..archive.len() {
183 let mut file: zip::read::ZipFile<'_, File> =
184 archive.by_index(i).expect("ZIP file invalid.");
185 if !file.is_dir() && JSON_FILE_NAME.is_match(file.name()) {
186 counter += 1;
187 println!("Analyzing file #{}: {}", counter, file.name());
188 let mut buffer: String = String::new();
189 let read_result = file.read_to_string(&mut buffer);
190 if read_result.is_ok() {
191 let messages: Vec<Message> = read_file(file.name(), buffer.as_str());
192 println!(
193 "Read {:?} bytes into {} messages.",
194 read_result.unwrap_or(0),
195 messages.len()
196 );
197 let messages_in_channel: Vec<MessageInChannel> = messages
198 .into_iter()
199 .map(|x| MessageInChannel::new(file.name(), x))
200 .collect();
201 result.extend(messages_in_channel);
202 }
203 }
204 }
205 println!(
206 "Read {} messages from {} files in archive at '{}', sorting by time.",
207 result.len(),
208 counter,
209 zip_path.to_str().unwrap()
210 );
211 let mut sorted_results: Vec<MessageInChannel> = result.into_iter().collect();
212 sorted_results.sort_by_key(|x| x.message.time().timestamp_micros());
213 return sorted_results;
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn ts_to_datetime_ok() {
222 let msg1 = Message::new("tester", "123.456", "");
223 assert_eq!(
224 msg1.time(),
225 Utc.with_ymd_and_hms(1970, 1, 1, 0, 2, 3).unwrap()
226 );
227 let msg2 = Message::new("tester", "1234567", "");
228 assert_eq!(
229 msg2.time(),
230 Utc.with_ymd_and_hms(1970, 1, 15, 6, 56, 7).unwrap()
231 );
232 }
233
234 #[test]
235 #[should_panic(expected = "First part of timestamp is not an integer")]
236 fn ts_to_datetime_err() {
237 let invalid_time = Message::new("tester", "", "");
238 invalid_time.time();
239 }
240}