Skip to main content

eve_log_parser/
parser.rs

1/*!
2This module will do the parsing of eve logs.
3
4For now it's needed to have a specific synthax for the logs to be readable.
5More work will be needed to make the parser universal.
6*/
7
8use chrono::{DateTime, NaiveDateTime, Utc};
9use log::{debug, error, info};
10use regex::{Captures, Regex};
11
12use crate::models::*;
13
14const HEADER_REGEX: &str = r"(?m)^------------------------------------------------------------$
15^  Gamelog$
16^  Listener: (?P<name>.+)$
17^  Session Started: (?<timestamp>\d{4}.\d{2}.\d{2} \d{2}:\d{2}:\d{2})$
18^------------------------------------------------------------$";
19
20const DAMAGE_REXEX: &str = r"(?i)\[ (?P<timestamp>\d{4}.\d{2}.\d{2} \d{2}:\d{2}:\d{2}) \] \(combat\) <color=0x[0-9a-f]{8}><b>(?P<damage>\d+)</b> <color=0x[0-9a-f]{8}><font size=\d+>(?P<destination>(to|from))</font> <b><color=0x[0-9a-f]{8}>(?P<pilot>.+)\[(?P<ticker>.+)\]\((?P<shiptype>.+)\)</b><font size=\d+><color=0x[0-9a-f]{8}> - (?P<weapon>.+) - ((Smashes)|(Penetrates)|(Hits)|(Glances Off)|(Grazes))\n";
21
22const LOGI_REGEX: &str = r"(?i)^\[ (?P<timestamp>\d{4}.\d{2}.\d{2} \d{2}:\d{2}:\d{2}) \] \(combat\) <color=0x[0-9a-f]{8}><b>(?P<damage>\d+)</b><color=0x[0-9a-f]{8}><font size=\d+> remote ((armor)|(shield)|(hull)) .+ (?P<destination>(by|to)) </font><b><color=0x[0-9a-f]{8}><font size=\d+><color=0x[0-9a-f]{8}> <b>(?P<shiptype>.+)</b></color></font><color=0x[0-9a-f]{8}> \[(?P<pilot>.+)\]<color=0x[0-9a-f]{8}><b> -</color> </b><color=0x[0-9a-f]{8}><font size=\d+> - (?P<reptype>.+)</font>\n";
23
24/// Read any log line from eve and creates an appropriate log if possible
25///
26/// # Example:
27/// ```
28/// use chrono::{TimeZone, Utc};
29/// use eve_log_parser::models::{Log, DamageLog, Destination};
30/// use eve_log_parser::parse_log_line;
31///
32///
33/// let log: String = "[ 2024.07.02 20:31:28 ] (combat) <color=0xff00ffff><b>200</b> <color=0x77ffffff><font size=10>to</font> <b><color=0xffffffff>Hornet EC-300[-15.0](Hornet EC-300)</b><font size=10><color=0x77ffffff> - Draclira's Modified Tachyon Beam Laser - Penetrates\n".to_string();        
34/// let expected_output = Log::Damage(DamageLog::new(
35///     Utc.with_ymd_and_hms(2024, 7, 2, 20, 31, 28).unwrap(),
36///     200,
37///     "Hornet EC-300".to_string(),
38///     "Hornet EC-300".to_string(),
39///     "Draclira's Modified Tachyon Beam Laser".to_string(),
40///     Destination::Dealing,
41/// ));
42///
43/// let output = parse_log_line(&log).unwrap();
44///
45/// assert_eq!(expected_output, output);
46/// ```
47pub fn parse_log_line(text: &String) -> Option<Log> {
48    info!("Parsing {}", text);
49    let damage_re: Regex = Regex::new(DAMAGE_REXEX).unwrap();
50    let logi_re: Regex = Regex::new(LOGI_REGEX).unwrap();
51
52    if let Some(capture) = damage_re.captures(text) {
53        debug!("Damage log recognized");
54        make_damage_log_from_capture(&capture)
55    } else if let Some(capture) = logi_re.captures(text) {
56        debug!("Logi log recognized");
57        make_logi_log_from_capture(&capture)
58    } else {
59        debug!("Log type not recognized");
60        None
61    }
62}
63
64/// Extracts the character name and the logfile's beginning from its header
65pub fn parse_log_header(header: String) -> Option<(String, DateTime<Utc>)> {
66    info!("Parsing header {}", header);
67    let header_re = Regex::new(HEADER_REGEX).unwrap();
68
69    if let Some(capture) = header_re.captures(header.as_str()) {
70        debug!("Recognized the header");
71        let log_beginning = parse_datetime(&capture["timestamp"]);
72        let character_name = capture["name"].to_string();
73        Some((character_name, log_beginning))
74    } else {
75        debug!("Didn't recognize the header format");
76        None
77    }
78}
79
80/// Takes the caputre of a regex and tries to create a Damage log out of it
81fn make_damage_log_from_capture(capture: &Captures) -> Option<Log> {
82    if let Ok(damage) = capture["damage"].parse::<isize>() {
83        let destination = match capture["destination"].as_ref() {
84            "to" => Destination::Dealing,
85            "from" => Destination::Receiving,
86            _ => panic!("Unexpected token received - {}", &capture["destination"]),
87        };
88        Some(Log::Damage(DamageLog::new(
89            parse_datetime(&capture["timestamp"]),
90            damage,
91            capture["pilot"].to_string(),
92            capture["shiptype"].to_string(),
93            capture["weapon"].to_string(),
94            destination,
95        )))
96    } else {
97        error!("Couldn't parse the damage - {}", &capture["damage"]);
98        None
99    }
100}
101
102/// Takes the caputre of a regex and tries to create a Logi log out of it
103fn make_logi_log_from_capture(capture: &Captures) -> Option<Log> {
104    if let Ok(amount) = capture["damage"].parse::<isize>() {
105        let destination = match capture["destination"].as_ref() {
106            "by" => Destination::Receiving,
107            "to" => Destination::Dealing,
108            _ => panic!("Unexpected token received - {}", &capture["destination"]),
109        };
110        Some(Log::Logi(LogiLog::new(
111            parse_datetime(&capture["timestamp"]),
112            amount,
113            capture["pilot"].to_string(),
114            capture["shiptype"].to_string(),
115            capture["reptype"].to_string(),
116            destination,
117        )))
118    } else {
119        error!(
120            "Couldn't parse the amount - {}",
121            capture["amount"].to_string()
122        );
123        None
124    }
125}
126
127/// Recovers the datetime value from
128fn parse_datetime(string: &str) -> DateTime<Utc> {
129    let naive_datetime = NaiveDateTime::parse_from_str(string, "%Y.%m.%d %H:%M:%S").unwrap();
130    DateTime::<Utc>::from_naive_utc_and_offset(naive_datetime, Utc)
131}
132
133#[cfg(test)]
134mod tests {
135    use chrono::{TimeZone, Utc};
136
137    use crate::parser::{Destination, LogiLog};
138
139    use super::{parse_log_header, parse_log_line, DamageLog, Log};
140
141    #[test]
142    fn test_basic_damage_logs() {
143        let log_string1 = "[ 2024.07.02 20:31:28 ] (combat) <color=0xff00ffff><b>200</b> <color=0x77ffffff><font size=10>to</font> <b><color=0xffffffff>Hornet EC-300[-15.0](Hornet EC-300)</b><font size=10><color=0x77ffffff> - Draclira's Modified Tachyon Beam Laser - Penetrates\n".to_string();
144        let log_string2 = "[ 2024.06.25 15:20:01 ] (combat) <color=0xffcc0000><b>375</b> <color=0x77ffffff><font size=10>from</font> <b><color=0xffffffff>Tek'wka Rokym[WH.SQ](Paladin)</b><font size=10><color=0x77ffffff> - Imperial Navy Large EMP Smartbomb - Hits\n".to_string();
145        let log_string3 = "[ 2024.07.02 19:42:05 ] (combat) <color=0xff00ffff><b>153</b> <color=0x77ffffff><font size=10>to</font> <b><color=0xffffffff>Kilyavi Alaailaa[-15.0](Capsule)</b><font size=10><color=0x77ffffff> - Medium Vorton Projector II - Hits\n".to_string();
146
147        let parser_output1 = parse_log_line(&log_string1).unwrap();
148        let parser_output2 = parse_log_line(&log_string2).unwrap();
149        let parser_output3 = parse_log_line(&log_string3).unwrap();
150
151        let expected_output1 = DamageLog::new(
152            Utc.with_ymd_and_hms(2024, 7, 2, 20, 31, 28).unwrap(),
153            200,
154            "Hornet EC-300".to_string(),
155            "Hornet EC-300".to_string(),
156            "Draclira's Modified Tachyon Beam Laser".to_string(),
157            Destination::Dealing,
158        );
159        let expected_output2 = DamageLog::new(
160            Utc.with_ymd_and_hms(2024, 06, 25, 15, 20, 01).unwrap(),
161            375,
162            "Tek'wka Rokym".to_string(),
163            "Paladin".to_string(),
164            "Imperial Navy Large EMP Smartbomb".to_string(),
165            Destination::Receiving,
166        );
167        let expected_output3 = DamageLog::new(
168            Utc.with_ymd_and_hms(2024, 07, 02, 19, 42, 05).unwrap(),
169            153,
170            "Kilyavi Alaailaa".to_string(),
171            "Capsule".to_string(),
172            "Medium Vorton Projector II".to_string(),
173            Destination::Dealing,
174        );
175
176        assert_eq!(parser_output1, Log::Damage(expected_output1));
177        assert_eq!(parser_output2, Log::Damage(expected_output2));
178        assert_eq!(parser_output3, Log::Damage(expected_output3));
179    }
180
181    #[test]
182    fn test_basic_logi_logs() {
183        // TODO still need to test logi output
184        let log_string1 = "[ 2024.07.02 19:13:23 ] (combat) <color=0xffccff66><b>772</b><color=0x77ffffff><font size=10> remote shield boosted by </font><b><color=0xffffffff><font size=14><color=0xFFFFFFFF> <b>Osprey</b></color></font><color=0xFFB3B3B3> [Drentu]<color=0xFFFFFFFF><b> -</color> </b><color=0x77ffffff><font size=10> - Medium Ancillary Remote Shield Booster</font>\n".to_string();
185        let log_string2 = "[ 2024.07.02 20:14:35 ] (combat) <color=0xffccff66><b>665</b><color=0x77ffffff><font size=10> remote shield boosted by </font><b><color=0xffffffff><font size=14><color=0xFF70FF40> <b>Scimitar</b></color></font><color=0xFFFF4040> [Drentu]<color=0xFFFFFFFF><b> -</color> </b><color=0x77ffffff><font size=10> - Large Remote Shield Booster II</font>\n".to_string();
186
187        let parser_output1 = parse_log_line(&log_string1).unwrap();
188        let parser_output2 = parse_log_line(&log_string2).unwrap();
189
190        let expected_output1 = LogiLog::new(
191            Utc.with_ymd_and_hms(2024, 7, 2, 19, 13, 23).unwrap(),
192            772,
193            "Drentu".to_string(),
194            "Osprey".to_string(),
195            "Medium Ancillary Remote Shield Booster".to_string(),
196            Destination::Receiving,
197        );
198        let expected_output2 = LogiLog::new(
199            Utc.with_ymd_and_hms(2024, 07, 02, 20, 14, 35).unwrap(),
200            665,
201            "Drentu".to_string(),
202            "Scimitar".to_string(),
203            "Large Remote Shield Booster II".to_string(),
204            Destination::Receiving,
205        );
206
207        assert_eq!(parser_output1, Log::Logi(expected_output1));
208        assert_eq!(parser_output2, Log::Logi(expected_output2));
209    }
210
211    #[test]
212    fn test_header_parsing() {
213        let header1 = "------------------------------------------------------------
214  Gamelog
215  Listener: T'rahk Rokym
216  Session Started: 2024.07.02 19:41:40
217------------------------------------------------------------\n"
218            .to_string();
219
220        let expected_output1 = Some((
221            "T'rahk Rokym".to_string(),
222            Utc.with_ymd_and_hms(2024, 07, 02, 19, 41, 40).unwrap(),
223        ));
224
225        let output1 = parse_log_header(header1);
226
227        assert_eq!(output1, expected_output1)
228    }
229}