Skip to main content

wechat_protocol/calls/client/
termination.rs

1//! Direct, main-context roster transitions. Callers first authenticate a whole
2//! newer roster, preserve original identities and exclude escalation contexts.
3use super::personal::RemoteExitReason;
4use anyhow::{bail, ensure, Result};
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub enum LocalStopReason {
8    SelfRemoved,
9    HandledElsewhere { sdk_end: u32 },
10    PeerLeftBeforeJoin,
11}
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum RemoteTermination {
14    LocalStop(LocalStopReason),
15    Exit(RemoteExitReason),
16    NoResponseCancel,
17}
18#[derive(Clone, Copy)]
19pub(super) enum Origin {
20    Caller,
21    Callee,
22}
23
24/// `None` is absence from the authenticated full list, never a fabricated row.
25/// Device-type changes and timestamps are not takeover evidence.
26pub(super) fn classify(
27    origin: Origin,
28    local_status: u32,
29    own: Option<u32>,
30    peer: Option<u32>,
31    pending_invitation: bool,
32) -> Result<Option<RemoteTermination>> {
33    ensure!(
34        own.is_none_or(|status| (1..=10).contains(&status))
35            && peer.is_none_or(|status| (1..=10).contains(&status)),
36        "Unsupported call member status"
37    );
38    if local_status == 1 && own != Some(1) {
39        return Ok(Some(RemoteTermination::LocalStop(
40            LocalStopReason::SelfRemoved,
41        )));
42    }
43    match origin {
44        Origin::Caller => ensure!(local_status == 1, "Unsupported caller local state"),
45        Origin::Callee => {
46            ensure!(
47                (1..=3).contains(&local_status),
48                "Unsupported callee local state"
49            );
50            if local_status != 1 {
51                if let Some(status @ (1 | 4 | 6 | 7 | 8)) = own {
52                    let sdk_end = match status {
53                        1 => 13,
54                        6 => 15,
55                        7 => 14,
56                        _ => 8,
57                    };
58                    return Ok(Some(RemoteTermination::LocalStop(
59                        LocalStopReason::HandledElsewhere { sdk_end },
60                    )));
61                }
62                ensure!(
63                    matches!(own, Some(2 | 3)),
64                    "Unsupported pending callee membership"
65                );
66            }
67        }
68    }
69    if matches!(peer, Some(1..=3)) {
70        return Ok(None);
71    }
72    if matches!(origin, Origin::Callee) && local_status != 1 {
73        return Ok(Some(RemoteTermination::LocalStop(
74            LocalStopReason::PeerLeftBeforeJoin,
75        )));
76    }
77    let reason = match (origin, peer) {
78        (Origin::Caller, Some(6)) => RemoteExitReason::PeerBusy,
79        (Origin::Caller, Some(7)) => RemoteExitReason::PeerRejected,
80        (Origin::Caller, Some(8)) => {
81            ensure!(
82                pending_invitation,
83                "Missing pending invitation for no-response cancellation"
84            );
85            return Ok(Some(RemoteTermination::NoResponseCancel));
86        }
87        (_, None | Some(4..=10)) => RemoteExitReason::LastPeerLeft,
88        _ => bail!("Unsupported remote member transition"),
89    };
90    Ok(Some(RemoteTermination::Exit(reason)))
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    #[test]
97    fn pending_callee_handles_elsewhere_only_for_source_predicate() {
98        for local in [2, 3] {
99            for (own, end) in [(1, 13), (4, 8), (6, 15), (7, 14), (8, 8)] {
100                assert_eq!(
101                    classify(Origin::Callee, local, Some(own), Some(1), false).unwrap(),
102                    Some(RemoteTermination::LocalStop(
103                        LocalStopReason::HandledElsewhere { sdk_end: end }
104                    ))
105                );
106            }
107        }
108        assert!(classify(Origin::Callee, 3, Some(5), Some(1), false).is_err());
109        assert_eq!(
110            classify(Origin::Callee, 3, Some(3), Some(1), false).unwrap(),
111            None
112        );
113    }
114    #[test]
115    fn unsupported_status_or_origin_does_not_become_terminal_proof() {
116        assert!(classify(Origin::Caller, 1, Some(1), Some(11), true).is_err());
117        assert!(classify(Origin::Callee, 3, None, Some(1), false).is_err());
118        assert!(classify(Origin::Caller, 3, Some(3), Some(4), true).is_err());
119    }
120}