nwws_oi/
message.rs

1/// A message received from NWWS-OI.
2///
3/// See the [NWS Communications Header Policy Document](https://www.weather.gov/tg/awips) for
4/// information about how to interpret this data.
5#[derive(Debug, Clone, Eq, PartialEq)]
6pub struct Message {
7    /// The six character WMO product ID
8    pub ttaaii: String,
9
10    /// Four character issuing center
11    pub cccc: String,
12
13    /// The six character AWIPS ID, sometimes called AFOS PIL
14    pub awips_id: Option<String>,
15
16    /// The time at which this product was issued
17    pub issue: chrono::DateTime<chrono::FixedOffset>,
18
19    /// A unique ID for this message
20    ///
21    /// The id contains two numbers separated by a period. The first number is the UNIX process ID
22    /// on the system running the ingest process. The second number is a simple incremented
23    /// sequence number for the product. Gaps in the sequence likely indicate message loss.
24    pub id: String,
25
26    /// The time at which the message was originally sent by the NWS ingest process to the NWWS-OI
27    /// XMPP server, if it differs substantially from the current time.
28    ///
29    /// See [XEP-0203](https://xmpp.org/extensions/xep-0203.html) for more details.
30    pub delay_stamp: Option<chrono::DateTime<chrono::FixedOffset>>,
31
32    /// The LDM sequence number assigned to this product.
33    ///
34    /// [LDM documentation] states that this value is "[i]gnored by almost everything but existing
35    /// due to tradition and history". NWWS OI seems to always prepend such a sequence number to the
36    /// message body; this crate parses it out and places it here.
37    pub ldm_sequence_number: Option<u32>,
38
39    /// The contents of the message
40    pub message: String,
41}
42
43impl TryFrom<xmpp_parsers::minidom::Element> for Message {
44    type Error = ();
45
46    fn try_from(value: xmpp_parsers::minidom::Element) -> Result<Self, Self::Error> {
47        xmpp_parsers::message::Message::try_from(value)
48            .ok()
49            .and_then(|msg| Self::try_from(msg).ok())
50            .ok_or(())
51    }
52}
53
54impl TryFrom<xmpp_parsers::message::Message> for Message {
55    type Error = xmpp_parsers::message::Message;
56
57    fn try_from(value: xmpp_parsers::message::Message) -> std::result::Result<Self, Self::Error> {
58        if value.type_ != xmpp_parsers::message::MessageType::Groupchat {
59            return Err(value);
60        }
61
62        let delay_stamp = value
63            .payloads
64            .iter()
65            .find(|p| p.is("delay", "urn:xmpp:delay"))
66            .and_then(|delay| delay.attr("stamp"))
67            .and_then(|v| chrono::DateTime::parse_from_rfc3339(v).ok());
68
69        let oi = if let Some(oi) = value.payloads.iter().find(|p| p.is("x", "nwws-oi")) {
70            oi
71        } else {
72            return Err(value);
73        };
74
75        let message = oi.text();
76
77        // Some messages have every \n replaced with \n\n
78        // Detect and undo that transformation
79        let message = if message.matches("\n").count() == message.matches("\n\n").count() * 2 {
80            message.replace("\n\n", "\n")
81        } else {
82            message
83        };
84
85        // Fish out the LDM sequence number, if any
86        let (ldm_sequence_number, message) = match {
87            let mut i = message.splitn(3, '\n');
88            (i.next(), i.next().and_then(|s| s.parse().ok()), i.next())
89        } {
90            (Some(""), Some(ldm_sequence_number), Some(rest)) => {
91                (Some(ldm_sequence_number), rest.into())
92            }
93            _ => (None, message),
94        };
95
96        return match (
97            oi.attr("awipsid"),
98            oi.attr("cccc"),
99            oi.attr("id"),
100            oi.attr("issue").map(chrono::DateTime::parse_from_rfc3339),
101            oi.attr("ttaaii"),
102        ) {
103            (Some(awipsid), Some(cccc), Some(id), Some(Ok(issue)), Some(ttaaii)) => Ok(Self {
104                awips_id: Some(awipsid).filter(|s| s.len() > 0).map(|s| s.into()),
105                cccc: cccc.into(),
106                id: id.into(),
107                issue,
108                ttaaii: ttaaii.into(),
109                delay_stamp,
110                ldm_sequence_number,
111                message,
112            }),
113            _ => Err(value),
114        };
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use chrono::Timelike;
122
123    fn msg(xml: &str) -> Result<Message, ()> {
124        let element: xmpp_parsers::minidom::Element = xml.parse().unwrap();
125        let msg: xmpp_parsers::message::Message = element.try_into().unwrap();
126
127        Message::try_from(msg).map_err(|_| ())
128    }
129
130    #[test]
131    fn parse_banner() {
132        assert_eq!(
133            msg("<message xmlns=\"jabber:client\" from=\"nwws@conference.nwws-oi.weather.gov\" to=\"w.glynn@nwws-oi.weather.gov/todo\" type=\"groupchat\"><subject>National Weather Wire Service Open Interface</subject><delay xmlns=\"urn:xmpp:delay\" from=\"nwws@conference.nwws-oi.weather.gov\" stamp=\"2015-02-03T20:48:44.222Z\"/></message>"),
134            Err(())
135        );
136    }
137
138    #[test]
139    fn parse_terms() {
140        assert_eq!(
141            msg("<message xmlns=\"jabber:client\" from=\"nwws-oi.weather.gov\" to=\"w.glynn@nwws-oi.weather.gov/uuid/56d00e55-29f5-446a-8e18-0dd6af8e7dcd\"><subject>US Federal Government</subject><body>**WARNING**WARNING**WARNING**WARNING**WARNING**WARNING**WARNING**WARNING**\n\nThis is a United States Federal Government computer system, which may be\naccessed and used only for official Government business by authorized\npersonnel.  Unauthorized access or use of this computer system may\nsubject violators to criminal, civil, and/or administrative action.\n\nAll information on this computer system may be intercepted, recorded,\nread, copied, and disclosed by and to authorized personnel for official\npurposes, including criminal investigations. Access or use of this\ncomputer system by any person whether authorized or unauthorized,\nCONSTITUTES CONSENT to these terms.\n\n**WARNING**WARNING**WARNING**WARNING**WARNING**WARNING**WARNING**WARNING**</body></message>"),
142            Err(())
143        );
144    }
145
146    #[test]
147    fn parse_awips() {
148        let timestamp = chrono::DateTime::from_naive_utc_and_offset(
149            chrono::NaiveDate::from_ymd_opt(2022, 2, 4)
150                .unwrap()
151                .and_hms_opt(2, 54, 0)
152                .unwrap(),
153            chrono::FixedOffset::east_opt(0).unwrap(),
154        );
155
156        assert_eq!(
157            msg("<message xmlns=\"jabber:client\" to=\"w.glynn@nwws-oi.weather.gov/uuid/25976f21-a846-4e08-8890-d750a95d96a2\" type=\"groupchat\" from=\"nwws@conference.nwws-oi.weather.gov/nwws-oi\"><body>KLMK issues RRM valid 2022-02-04T02:54:00Z</body><html xmlns=\"http://jabber.org/protocol/xhtml-im\"><body xmlns=\"http://www.w3.org/1999/xhtml\">KLMK issues RRM valid 2022-02-04T02:54:00Z</body></html><x xmlns=\"nwws-oi\" cccc=\"KLMK\" ttaaii=\"SRUS43\" issue=\"2022-02-04T02:54:00Z\" awipsid=\"RRMLMK\" id=\"14425.25117\"><![CDATA[\n\n987\n\nSRUS43 KLMK 040254\n\nRRMLMK\n\n.ER PRSK2 20220203 Z DC202202040254/DUE/DQG/DH17/HGIFE/DIH1/\n\n.E1 15.4/15.6/15.8/16.1/16.5/17.0/17.6/18.1\n\n.E2 18.6/18.8/18.8/18.9/19.2/19.2/19.3/19.3\n\n.E3 19.2/19.2/19.2/19.1/19.0/19.0/18.8/18.7\n\n.E4 18.6/18.4/18.4/18.4/18.4/18.3/18.2/18.1\n\n.E5 18.1/18.0/17.9/17.9/17.9/17.7/17.7/17.6\n\n.E6 17.5/17.6/17.5/17.4/17.3/17.2/17.2/17.0\n\n]]></x><delay xmlns=\"urn:xmpp:delay\" stamp=\"2022-02-04T02:55:11.810Z\" from=\"nwws@conference.nwws-oi.weather.gov/nwws-oi\"/></message>"),
158            Ok(Message {
159                ttaaii: "SRUS43".into(),
160                cccc: "KLMK".into(),
161                awips_id: Some(
162                    "RRMLMK".into()
163                ),
164                issue: timestamp,
165                id: "14425.25117".into(),
166                delay_stamp: timestamp.with_minute(55).unwrap().with_second(11).unwrap().with_nanosecond(810_000_000),
167                ldm_sequence_number: Some(987),
168                message: "SRUS43 KLMK 040254\nRRMLMK\n.ER PRSK2 20220203 Z DC202202040254/DUE/DQG/DH17/HGIFE/DIH1/\n.E1 15.4/15.6/15.8/16.1/16.5/17.0/17.6/18.1\n.E2 18.6/18.8/18.8/18.9/19.2/19.2/19.3/19.3\n.E3 19.2/19.2/19.2/19.1/19.0/19.0/18.8/18.7\n.E4 18.6/18.4/18.4/18.4/18.4/18.3/18.2/18.1\n.E5 18.1/18.0/17.9/17.9/17.9/17.7/17.7/17.6\n.E6 17.5/17.6/17.5/17.4/17.3/17.2/17.2/17.0\n".into(),
169            })
170        );
171
172        assert_eq!(
173            msg("<message xmlns=\"jabber:client\" to=\"w.glynn@nwws-oi.weather.gov/uuid/851c737e-ead3-460d-b0a6-6749602fccd9\" type=\"groupchat\" from=\"nwws@conference.nwws-oi.weather.gov/nwws-oi\"><body>PAJK issues RR3 valid 2022-02-04T02:11:00Z</body><html xmlns=\"http://jabber.org/protocol/xhtml-im\"><body xmlns=\"http://www.w3.org/1999/xhtml\">PAJK issues RR3 valid 2022-02-04T02:11:00Z</body></html><x xmlns=\"nwws-oi\" cccc=\"PAJK\" ttaaii=\"SRAK57\" issue=\"2022-02-04T02:11:00Z\" awipsid=\"RR3AJK\" id=\"14425.24041\"><![CDATA[\n\n876\n\nSRAK57 PAJK 040211\n\nRR3AJK\n\nSRAK57 PAJK 040210\n\n\n\n.A NDIA2 220204 Z DH0202/TA 26/TD 27/UD 0/US 0/UG 0/UP 0/PA 29.57\n\n]]></x></message>"),
174            Ok(Message {
175                ttaaii: "SRAK57".into(),
176                cccc: "PAJK".into(),
177                awips_id: Some("RR3AJK".into()),
178                issue: timestamp.with_minute(11).unwrap(),
179                id: "14425.24041".into(),
180                delay_stamp: None,
181                ldm_sequence_number: Some(876),
182                message: "SRAK57 PAJK 040211\nRR3AJK\nSRAK57 PAJK 040210\n\n.A NDIA2 220204 Z DH0202/TA 26/TD 27/UD 0/US 0/UG 0/UP 0/PA 29.57\n".into(),
183            }));
184
185        assert_eq!(
186            msg("<message xmlns=\"jabber:client\" to=\"w.glynn@nwws-oi.weather.gov/uuid/851c737e-ead3-460d-b0a6-6749602fccd9\" type=\"groupchat\" from=\"nwws@conference.nwws-oi.weather.gov/nwws-oi\"><body>KKCI issues CFP valid 2022-02-04T02:00:00Z</body><html xmlns=\"http://jabber.org/protocol/xhtml-im\"><body xmlns=\"http://www.w3.org/1999/xhtml\">KKCI issues CFP valid 2022-02-04T02:00:00Z</body></html><x xmlns=\"nwws-oi\" cccc=\"KKCI\" ttaaii=\"FAUS29\" issue=\"2022-02-04T02:00:00Z\" awipsid=\"CFP03\" id=\"14425.22838\"><![CDATA[\n\n631\n\nFAUS29 KKCI 040200\n\nCFP03 \n\nCCFP 20220204_0200 20220204_0800\n\nCANADA OFF\n\n]]></x></message>"),
187            Ok(Message{
188                ttaaii: "FAUS29".to_string(),
189                cccc: "KKCI".to_string(),
190                awips_id: Some("CFP03".into()),
191                issue: timestamp.with_minute(0).unwrap(),
192                id: "14425.22838".into(),
193                delay_stamp: None,
194                ldm_sequence_number: Some(631),
195                message: "FAUS29 KKCI 040200\nCFP03 \nCCFP 20220204_0200 20220204_0800\nCANADA OFF\n".into()
196            }));
197    }
198
199    #[test]
200    fn parse_test() {
201        assert_eq!(
202            msg("<message xmlns=\"jabber:client\" to=\"w.glynn@nwws-oi.weather.gov/uuid/851c737e-ead3-460d-b0a6-6749602fccd9\" type=\"groupchat\" from=\"nwws@conference.nwws-oi.weather.gov/nwws-oi\"><body>PHEB issues  valid 2022-02-04T01:23:00Z</body><html xmlns=\"http://jabber.org/protocol/xhtml-im\"><body xmlns=\"http://www.w3.org/1999/xhtml\">PHEB issues  valid 2022-02-04T01:23:00Z</body></html><x xmlns=\"nwws-oi\" cccc=\"PHEB\" ttaaii=\"NTXX98\" issue=\"2022-02-04T01:23:00Z\" awipsid=\"\" id=\"14425.22800\"><![CDATA[\n\n593\n\nNTXX98 PHEB 040123\n\nPTWC REDUNDANT-SIDE TEST FROM IRC\n\nRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZ\n\nRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZ\n\n]]></x></message>"),
203            Ok(Message {
204                ttaaii: "NTXX98".into(),
205                cccc: "PHEB".into(),
206                awips_id: None,
207                issue: chrono::DateTime::from_naive_utc_and_offset(chrono::NaiveDate::from_ymd_opt(2022, 2, 4).unwrap().and_hms_opt(1, 23, 0).unwrap(), chrono::FixedOffset::east_opt(0).unwrap()),
208                id: "14425.22800".into(),
209                delay_stamp: None,
210                ldm_sequence_number: Some(593),
211                message: "NTXX98 PHEB 040123\nPTWC REDUNDANT-SIDE TEST FROM IRC\nRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZ\nRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZRZ\n".into(),
212            })
213        );
214    }
215}