Skip to main content

wechat_protocol/calls/
incoming.rs

1//! Offline incoming call carriers and conference notification envelopes.
2//!
3//! Decoding does not verify freshness, account ownership or room admission. The
4//! caller must retain the original notification and its projection obligations.
5//! The outer carrier and inner conference envelope are different wire messages;
6//! only the selected carrier field is forwarded to the conference parser.
7//! Room kinds and cancellation reasons remain numbers, without guessed names.
8
9use super::signaling::{decode_fields, optional_u32, unknown_fields};
10use crate::protobuf::{self as p, Field, Value};
11use anyhow::{bail, ensure, Context, Result};
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum CarrierKind {
15    Invite,
16    Cancel,
17}
18
19/// Opaque outer fields survive without assigning semantics to unnamed slots.
20/// These fields and the forwarded bytes can contain private call information;
21/// none of the data-bearing notification types implement `Debug`.
22pub struct NotificationCarrier {
23    kind: CarrierKind,
24    fields: Vec<Field>,
25}
26
27impl NotificationCarrier {
28    pub fn decode(kind: CarrierKind, input: &[u8]) -> Result<Self> {
29        let fields = decode_fields(input)?;
30        match kind {
31            CarrierKind::Invite => {
32                for number in [1, 3, 4, 5, 6] {
33                    p::optional_data(&fields, number)?;
34                }
35                optional_u64(&fields, 2)?;
36                for field in fields.iter().filter(|field| field.number == 7) {
37                    ensure!(
38                        matches!(field.value, Value::Bytes(_)),
39                        "Invalid invite member field"
40                    );
41                }
42            }
43            CarrierKind::Cancel => {
44                optional_u64(&fields, 1)?;
45                p::optional_data(&fields, 2)?;
46            }
47        }
48        Ok(Self { kind, fields })
49    }
50
51    pub fn kind(&self) -> CarrierKind {
52        self.kind
53    }
54
55    pub fn fields(&self) -> &[Field] {
56        &self.fields
57    }
58
59    /// Invite forwards outer field 6; cancellation forwards outer field 2.
60    /// Returning these bytes is not permission to submit or consume a control.
61    pub fn conference_bytes(&self) -> Result<&[u8]> {
62        let number = match self.kind {
63            CarrierKind::Invite => 6,
64            CarrierKind::Cancel => 2,
65        };
66        p::optional_data(&self.fields, number)?.context("Missing conference notification")
67    }
68
69    pub fn conference_notification(&self) -> Result<ConferenceNotification> {
70        let notification = ConferenceNotification::decode(self.conference_bytes()?)?;
71        ensure!(
72            matches!(
73                (self.kind, notification.kind),
74                (CarrierKind::Invite, Some(2)) | (CarrierKind::Cancel, Some(3))
75            ),
76            "Outer and conference notification kinds disagree"
77        );
78        Ok(notification)
79    }
80}
81
82/// Conference envelope. Unsupported kinds remain readable as raw numbers and
83/// bytes; [`Self::payload`] refuses to invent a supported control for them.
84pub struct ConferenceNotification {
85    pub kind: Option<u32>,
86    pub data: Option<Vec<u8>>,
87    pub unknown_fields: Vec<Field>,
88}
89
90impl ConferenceNotification {
91    pub fn decode(input: &[u8]) -> Result<Self> {
92        let fields = decode_fields(input)?;
93        Ok(Self {
94            kind: optional_u32(&fields, 1)?,
95            data: p::optional_data(&fields, 2)?.map(Vec::from),
96            unknown_fields: unknown_fields(fields, 2),
97        })
98    }
99
100    pub fn payload(&self) -> Result<NotificationPayload> {
101        let data = self.data.as_deref().context("Missing conference payload")?;
102        match self.kind {
103            Some(2) => Ok(NotificationPayload::Invite(InviteNotification::decode(
104                data,
105            )?)),
106            Some(3) => Ok(NotificationPayload::Cancel(CancelNotification::decode(
107                data,
108            )?)),
109            _ => bail!("Unsupported conference notification kind"),
110        }
111    }
112}
113
114pub enum NotificationPayload {
115    Invite(InviteNotification),
116    Cancel(CancelNotification),
117}
118
119/// Presence and full integer widths are preserved. This is not an admitted
120/// invitation: missing identities, unknown enums and peer/account bindings
121/// require independent validation before a controller can ring.
122pub struct InviteNotification {
123    pub room_id: Option<u64>,
124    pub room_key: Option<u64>,
125    pub member_id: Option<u32>,
126    pub room_type: Option<u32>,
127    pub caller_openid: Option<String>,
128    pub callee_openid: Option<String>,
129    pub caller_username: Option<String>,
130    pub callee_username: Option<String>,
131    pub usernames: Vec<String>,
132    pub app_id: Option<String>,
133    pub sub_app_id: Option<String>,
134    pub caller_member_id: Option<u32>,
135    pub wechat_group_id: Option<String>,
136    pub unknown_fields: Vec<Field>,
137}
138
139impl InviteNotification {
140    pub fn decode(input: &[u8]) -> Result<Self> {
141        let fields = decode_fields(input)?;
142        let usernames = fields
143            .iter()
144            .filter(|field| field.number == 9)
145            .map(|field| {
146                let Value::Bytes(value) = &field.value else {
147                    bail!("Invalid invite username field")
148                };
149                Ok(std::str::from_utf8(value)?.to_owned())
150            })
151            .collect::<Result<Vec<_>>>()?;
152        Ok(Self {
153            room_id: optional_u64(&fields, 1)?,
154            room_key: optional_u64(&fields, 2)?,
155            member_id: optional_u32(&fields, 3)?,
156            room_type: optional_u32(&fields, 4)?,
157            caller_openid: optional_string(&fields, 5)?,
158            callee_openid: optional_string(&fields, 6)?,
159            caller_username: optional_string(&fields, 7)?,
160            callee_username: optional_string(&fields, 8)?,
161            usernames,
162            app_id: optional_string(&fields, 10)?,
163            sub_app_id: optional_string(&fields, 11)?,
164            caller_member_id: optional_u32(&fields, 12)?,
165            wechat_group_id: optional_string(&fields, 13)?,
166            unknown_fields: unknown_fields(fields, 13),
167        })
168    }
169}
170
171pub struct CancelNotification {
172    pub room_id: Option<u64>,
173    pub room_key: Option<u64>,
174    pub member_id: Option<u32>,
175    pub reason: Option<u32>,
176    pub unknown_fields: Vec<Field>,
177}
178
179impl CancelNotification {
180    pub fn decode(input: &[u8]) -> Result<Self> {
181        let fields = decode_fields(input)?;
182        Ok(Self {
183            room_id: optional_u64(&fields, 1)?,
184            room_key: optional_u64(&fields, 2)?,
185            member_id: optional_u32(&fields, 3)?,
186            reason: optional_u32(&fields, 4)?,
187            unknown_fields: unknown_fields(fields, 4),
188        })
189    }
190}
191
192fn optional_u64(fields: &[Field], number: u32) -> Result<Option<u64>> {
193    match p::optional(fields, number)? {
194        None => Ok(None),
195        Some(Value::Varint(value)) => Ok(Some(*value)),
196        _ => bail!("Invalid notification scalar field {number}"),
197    }
198}
199
200fn optional_string(fields: &[Field], number: u32) -> Result<Option<String>> {
201    p::optional_data(fields, number)?
202        .map(|value| Ok(std::str::from_utf8(value)?.to_owned()))
203        .transpose()
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn cancellation_literal_uses_each_distinct_envelope() {
212        // Outer f1=99, f2=ThirdNotifyData(type=3, data=(42,43,7,4)).
213        let raw = [
214            0x08, 0x63, 0x12, 0x0c, 0x08, 0x03, 0x12, 0x08, 0x08, 0x2a, 0x10, 0x2b, 0x18, 0x07,
215            0x20, 0x04,
216        ];
217        let outer = NotificationCarrier::decode(CarrierKind::Cancel, &raw).unwrap();
218        assert_eq!(outer.kind(), CarrierKind::Cancel);
219        assert_eq!(outer.conference_bytes().unwrap(), &raw[4..]);
220        let third = outer.conference_notification().unwrap();
221        let NotificationPayload::Cancel(cancel) = third.payload().unwrap() else {
222            panic!("wrong kind")
223        };
224        assert_eq!(cancel.room_id, Some(42));
225        assert_eq!(cancel.room_key, Some(43));
226        assert_eq!(cancel.member_id, Some(7));
227        assert_eq!(cancel.reason, Some(4));
228        // The complete outer carrier is not itself the SDK envelope.
229        assert!(ConferenceNotification::decode(&raw)
230            .unwrap()
231            .payload()
232            .is_err());
233    }
234
235    #[test]
236    fn invite_preserves_widths_names_presence_and_unknowns() {
237        let raw = p::message(&[
238            p::uint(1, u64::MAX),
239            p::uint(2, u64::MAX - 1),
240            p::uint(3, u32::MAX.into()),
241            p::uint(4, 2),
242            p::bytes(5, b"caller-open"),
243            p::bytes(6, b"callee-open"),
244            p::bytes(7, b"synthetic-caller"),
245            p::bytes(8, b"synthetic-callee"),
246            p::bytes(9, b"one"),
247            p::bytes(9, b"two"),
248            p::bytes(10, b""),
249            p::uint(12, 0),
250            p::bytes(13, b"synthetic-group"),
251            p::bytes(99, b"unknown"),
252        ])
253        .unwrap();
254        let invite = InviteNotification::decode(&raw).unwrap();
255        assert_eq!(invite.room_id, Some(u64::MAX));
256        assert_eq!(invite.room_key, Some(u64::MAX - 1));
257        assert_eq!(invite.member_id, Some(u32::MAX));
258        assert_eq!(invite.room_type, Some(2));
259        assert_eq!(invite.caller_username.as_deref(), Some("synthetic-caller"));
260        assert_eq!(invite.callee_username.as_deref(), Some("synthetic-callee"));
261        assert_eq!(invite.usernames, vec!["one", "two"]);
262        assert_eq!(invite.app_id.as_deref(), Some(""));
263        assert!(invite.sub_app_id.is_none());
264        assert_eq!(invite.caller_member_id, Some(0));
265        assert_eq!(invite.unknown_fields.len(), 1);
266        assert_eq!(p::data(&invite.unknown_fields, 99).unwrap(), b"unknown");
267        let third = p::message(&[p::uint(1, 2), p::bytes(2, &raw), p::uint(99, 7)]).unwrap();
268        let outer = p::message(&[
269            p::bytes(1, b"opaque"),
270            p::uint(2, 9),
271            p::bytes(6, &third),
272            p::bytes(7, &p::uint(1, 7)),
273            p::uint(88, 3),
274        ])
275        .unwrap();
276        let outer = NotificationCarrier::decode(CarrierKind::Invite, &outer).unwrap();
277        assert_eq!(p::scalar(outer.fields(), 88).unwrap(), 3);
278        let third = outer.conference_notification().unwrap();
279        assert_eq!(p::scalar(&third.unknown_fields, 99).unwrap(), 7);
280        assert!(matches!(
281            third.payload().unwrap(),
282            NotificationPayload::Invite(_)
283        ));
284    }
285}