xmpp_parsers/
cam.rs

1// Copyright (c) 2026 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7use xso::{AsXml, FromXml};
8
9use crate::date::DateTime;
10use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
11use crate::ns;
12use minidom::Element;
13
14/// List clients that have access to the user’s account.
15#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
16#[xml(namespace = ns::CAM, name = "list")]
17pub struct List;
18
19impl IqGetPayload for List {}
20
21generate_attribute!(
22    /// Either "session" if this client is known to have an active or inactive client session on
23    /// the server, or "access" if no session has been established (e.g. it may have been granted
24    /// access to the account, but only used non-XMPP APIs or never logged in).
25    Type, "type", {
26        /// This client is known to have an active or inactive client session on the server.
27        Session => "session",
28
29        /// No session has been established (e.g. it may have been granted access to the account,
30        /// but only used non-XMPP APIs or never logged in).
31        Access => "access",
32    }
33);
34
35/// Information about the client software.
36#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
37#[xml(namespace = ns::CAM, name = "user-agent")]
38pub struct UserAgent {
39    /// The name of the software.
40    #[xml(extract(fields(text)))]
41    pub software: String,
42
43    /// A URI/URL for the client, such as a homepage.
44    #[xml(extract(default, fields(text(type_ = String))))]
45    pub uri: Option<String>,
46
47    /// A human-readable identifier/name for the device where the client runs.
48    #[xml(extract(default, fields(text(type_ = String))))]
49    pub device: Option<String>,
50}
51
52/// Lists the known authentication methods that the client has used to gain access to the account.
53#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
54#[xml(namespace = ns::CAM, name = "auth")]
55pub struct Auth {
56    /// The client has presented a valid password.
57    #[xml(flag)]
58    pub password: bool,
59
60    /// The client has a valid authorization grant (e.g. via OAuth).
61    #[xml(flag)]
62    pub grant: bool,
63
64    /// The client has active FAST tokens.
65    #[xml(flag)]
66    pub fast: bool,
67
68    /// The `<auth/>` element is explicitly extensible - alternative/future authentication
69    /// mechanisms may be included under appropriate namespaces.
70    #[xml(child(n = ..))]
71    pub other: Vec<Element>,
72}
73
74generate_attribute!(
75    /// Details of the client’s level of access to the user’s account.
76    Status, "status", {
77        /// The client has full unlimited access to the account.
78        Unrestricted => "unrestricted",
79
80        /// The client has general access to the account, but some security-relevant features may
81        /// be restricted (such as managing account access and changing the account password).
82        Normal => "normal",
83
84        /// the client has additional restrictions in place. In such a case the details of these
85        /// restrictions SHOULD be included in an appropriate format (and namespace) within the
86        /// `<permission/>` element.
87        Restricted => "restricted",
88    }
89);
90
91/// Contains details of the client’s level of access to the user’s account.
92#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
93#[xml(namespace = ns::CAM, name = "permission")]
94pub struct Permission {
95    /// The actual permission status.
96    #[xml(attribute)]
97    pub status: Status,
98}
99
100/// Represents a client that is know by the server.
101#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
102#[xml(namespace = ns::CAM, name = "client")]
103pub struct Client {
104    /// Reflects whether this client has an active session on the server ("active" includes
105    /// connected and sessions that may be disconnected but may yet be reconnected, e.g. using
106    /// Stream Management (XEP-0198)).
107    #[xml(attribute)]
108    pub connected: bool,
109
110    /// An opaque reference for the client, which can be used to revoke access.
111    #[xml(attribute)]
112    pub id: String,
113
114    /// Either "session" if this client is known to have an active or inactive client session on
115    /// the server, or "access" if no session has been established (e.g. it may have been granted
116    /// access to the account, but only used non-XMPP APIs or never logged in).
117    #[xml(attribute = "type")]
118    pub type_: Type,
119
120    /// Contains timestamps that reflect when this client was first granted access to the user’s
121    /// account.
122    #[xml(extract(default, name = "first-seen", fields(text(type_ = DateTime))))]
123    pub first_seen: Option<DateTime>,
124
125    /// Contains timestamps that reflect when this client most recently used that access.
126    #[xml(extract(default, name = "last-seen", fields(text(type_ = DateTime))))]
127    pub last_seen: Option<DateTime>,
128
129    /// Information about the client software.
130    #[xml(child)]
131    pub user_agent: UserAgent,
132
133    /// Lists the known authentication methods that the client has used to gain access to the
134    /// account.
135    #[xml(child)]
136    pub auth: Auth,
137
138    /// Contains details of the client’s level of access to the user’s account.
139    #[xml(child)]
140    pub permission: Permission,
141}
142
143/// Container for a list of known clients.
144#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
145#[xml(namespace = ns::CAM, name = "clients")]
146pub struct Clients {
147    /// The list of clients known by the server.
148    #[xml(child(n = ..))]
149    pub clients: Vec<Client>,
150}
151
152impl IqResultPayload for Clients {}
153
154/// Request to revoke access to a client by id.
155#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
156#[xml(namespace = ns::CAM, name = "revoke")]
157pub struct Revoke {
158    /// One of the client ids fetched from the list.
159    #[xml(attribute)]
160    pub id: String,
161}
162
163impl IqSetPayload for Revoke {}
164
165/// If the identified client has previously authenticated with a password, there is no way to
166/// revoke access except by changing the user’s password. If you request revocation of such a
167/// client, the server will respond with a 'service-unavailable' error, with the
168/// 'password-reset-required' application error:
169#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
170#[xml(namespace = ns::CAM, name = "password-reset-required")]
171pub struct PasswordResetRequiredError;
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use minidom::Element;
177
178    #[cfg(target_pointer_width = "32")]
179    #[test]
180    fn test_size() {
181        assert_size!(List, 0);
182        assert_size!(Type, 1);
183        assert_size!(UserAgent, 36);
184        assert_size!(Auth, 16);
185        assert_size!(Status, 1);
186        assert_size!(Permission, 1);
187        assert_size!(Client, 100);
188        assert_size!(Clients, 12);
189        assert_size!(Revoke, 12);
190        assert_size!(PasswordResetRequiredError, 0);
191    }
192
193    #[cfg(target_pointer_width = "64")]
194    #[test]
195    fn test_size() {
196        assert_size!(List, 0);
197        assert_size!(Type, 1);
198        assert_size!(UserAgent, 72);
199        assert_size!(Auth, 32);
200        assert_size!(Status, 1);
201        assert_size!(Permission, 1);
202        assert_size!(Client, 168);
203        assert_size!(Clients, 24);
204        assert_size!(Revoke, 24);
205        assert_size!(PasswordResetRequiredError, 0);
206    }
207
208    #[test]
209    fn test_example_2() {
210        let elem: Element = "<clients xmlns='urn:xmpp:cam:0'>
211    <client connected='true' id='zeiP41HLglIu' type='session'>
212      <first-seen>2023-04-06T14:26:08Z</first-seen>
213      <last-seen>2023-04-06T14:37:25Z</last-seen>
214      <auth>
215        <password/>
216      </auth>
217      <permission status='unrestricted'/>
218      <user-agent>
219        <software>Gajim</software>
220        <uri>https://gajim.org/</uri>
221        <device>Juliet's laptop</device>
222      </user-agent>
223    </client>
224    <client connected='false' id='HjEEr45_LQr' type='access'>
225      <first-seen>2023-03-27T15:16:09Z</first-seen>
226      <last-seen>2023-03-27T15:37:24Z</last-seen>
227      <auth>
228        <grant/>
229      </auth>
230      <permission status='normal'/>
231      <user-agent>
232        <software>REST client</software>
233      </user-agent>
234    </client>
235  </clients>"
236            .parse()
237            .unwrap();
238        let clients = Clients::try_from(elem).unwrap();
239        let client = &clients.clients[0];
240        assert_eq!(client.connected, true);
241        assert_eq!(client.id, "zeiP41HLglIu");
242        assert_eq!(client.type_, Type::Session);
243        assert_eq!(
244            client.first_seen,
245            Some("2023-04-06T14:26:08Z".parse().unwrap())
246        );
247        assert_eq!(
248            client.last_seen,
249            Some("2023-04-06T14:37:25Z".parse().unwrap())
250        );
251        assert_eq!(client.auth.password, true);
252        assert_eq!(client.auth.grant, false);
253        assert_eq!(client.auth.fast, false);
254        assert!(client.auth.other.is_empty());
255        assert_eq!(client.permission.status, Status::Unrestricted);
256        assert_eq!(client.user_agent.software, "Gajim");
257        assert_eq!(
258            client.user_agent.uri,
259            Some(String::from("https://gajim.org/"))
260        );
261        assert_eq!(
262            client.user_agent.device,
263            Some(String::from("Juliet's laptop"))
264        );
265
266        let client = &clients.clients[1];
267        assert_eq!(client.connected, false);
268        assert_eq!(client.id, "HjEEr45_LQr");
269        assert_eq!(client.type_, Type::Access);
270        assert_eq!(
271            client.first_seen,
272            Some("2023-03-27T15:16:09Z".parse().unwrap())
273        );
274        assert_eq!(
275            client.last_seen,
276            Some("2023-03-27T15:37:24Z".parse().unwrap())
277        );
278        assert_eq!(client.auth.password, false);
279        assert_eq!(client.auth.grant, true);
280        assert_eq!(client.auth.fast, false);
281        assert!(client.auth.other.is_empty());
282        assert_eq!(client.permission.status, Status::Normal);
283        assert_eq!(client.user_agent.software, "REST client");
284        assert!(client.user_agent.uri.is_none());
285        assert!(client.user_agent.device.is_none());
286    }
287}