Skip to main content

xmpp/muc/
room.rs

1// Copyright (c) 2023 xmpp-rs contributors.
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 crate::{
8    Agent, RoomNick,
9    jid::{BareJid, ResourceRef},
10    message::send::RawMessageSettings,
11    parsers::{
12        message::MessageType,
13        muc::Muc,
14        presence::{Presence, Type as PresenceType},
15    },
16};
17
18#[derive(Clone, Debug)]
19pub struct JoinRoomSettings<'a> {
20    pub room: BareJid,
21    pub nick: Option<RoomNick>,
22    pub password: Option<String>,
23    pub status: Option<(&'a str, &'a str)>,
24    /// Send the join payload again even though we're already supposedly joined.
25    /// This was made explicit in version 1.27 of XEP-0045 (cee524dbde4, 2016), that is just rejoin
26    /// with the standard MUC payload to force a resync of the client state (receive all occupants
27    /// and subject again).
28    pub force_resync: bool,
29}
30
31impl<'a> JoinRoomSettings<'a> {
32    pub fn new(room: BareJid) -> Self {
33        Self {
34            room,
35            nick: None,
36            password: None,
37            status: None,
38            force_resync: false,
39        }
40    }
41
42    pub fn with_nick(mut self, nick: impl AsRef<ResourceRef>) -> Self {
43        self.nick = Some(RoomNick::from_resource_ref(nick.as_ref()));
44        self
45    }
46
47    pub fn with_password(mut self, password: impl AsRef<str>) -> Self {
48        self.password = Some(password.as_ref().into());
49        self
50    }
51
52    pub fn with_status(mut self, lang: &'a str, content: &'a str) -> Self {
53        self.status = Some((lang, content));
54        self
55    }
56
57    pub fn with_resync(mut self) -> Self {
58        self.force_resync = true;
59        self
60    }
61}
62
63/// TODO: this method should add bookmark and ensure autojoin is true
64pub async fn join_room<'a>(agent: &mut Agent, settings: JoinRoomSettings<'a>) {
65    let JoinRoomSettings {
66        room,
67        nick,
68        password,
69        status,
70        force_resync,
71    } = settings;
72
73    if agent.rooms_joining.contains_key(&room) {
74        // We are already joining
75        warn!("Requesting to join again room {room} which is already joining...");
76        return;
77    }
78
79    if !force_resync && agent.rooms_joined.contains_key(&room) {
80        // We are already joined, cannot join
81        warn!("Requesting to join room {room} which is already joined...");
82        return;
83    }
84
85    let mut muc = Muc::new();
86    if let Some(password) = password {
87        muc = muc.with_password(password);
88    }
89
90    let nick = if let Some(nick) = nick {
91        nick
92    } else {
93        agent.get_config().await.default_nick
94    };
95
96    let room_jid = room.with_resource(&nick);
97    let mut presence = Presence::new(PresenceType::None).with_to(room_jid);
98    presence.add_payload(muc);
99
100    let (lang, status) = status.unwrap_or(("", ""));
101    presence.set_status(String::from(lang), String::from(status));
102
103    let _ = agent.client.send_stanza(presence.into()).await;
104
105    agent.rooms_joining.insert(room, nick);
106}
107
108#[derive(Clone, Debug)]
109pub struct LeaveRoomSettings<'a> {
110    pub room: BareJid,
111    pub status: Option<(&'a str, &'a str)>,
112}
113
114impl<'a> LeaveRoomSettings<'a> {
115    pub fn new(room: BareJid) -> Self {
116        Self { room, status: None }
117    }
118
119    pub fn with_status(mut self, lang: &'a str, content: &'a str) -> Self {
120        self.status = Some((lang, content));
121        self
122    }
123}
124
125/// Send a "leave room" request to the server (specifically, an "unavailable" presence stanza).
126///
127/// The returned future will resolve when the request has been sent,
128/// not when the room has actually been left.
129///
130/// If successful, a `RoomLeft` event should be received later as a confirmation. See [XEP-0045](https://xmpp.org/extensions/xep-0045.html#exit).
131///
132/// TODO: this method should set autojoin false on bookmark
133pub async fn leave_room<'a>(agent: &mut Agent, settings: LeaveRoomSettings<'a>) {
134    let LeaveRoomSettings { room, status } = settings;
135
136    if agent.rooms_leaving.contains_key(&room) {
137        // We are already leaving
138        warn!("Requesting to leave again room {room} which is already leaving...");
139        return;
140    }
141
142    if !agent.rooms_joined.contains_key(&room) {
143        // We are not joined, cannot leave
144        warn!("Requesting to leave room {room} which is not joined...");
145        return;
146    }
147
148    // Get currently-used nickname
149    let nickname = agent.rooms_joined.get(&room).unwrap();
150
151    // XEP-0045 specifies that, to leave a room, the client must send a presence stanza
152    // with type="unavailable".
153    let mut presence =
154        Presence::new(PresenceType::Unavailable).with_to(room.with_resource(&nickname));
155
156    // Optionally, the client may include a status message in the presence stanza.
157    // TODO: Should this be optional? The XEP says "MAY", but the method signature requires the arguments.
158    // XEP-0045: "The occupant MAY include normal <status/> information in the unavailable presence stanzas"
159    if let Some((lang, content)) = status {
160        presence.set_status(lang, content);
161    }
162
163    // Send the presence stanza.
164    if let Err(e) = agent.client.send_stanza(presence.into()).await {
165        // Report any errors to the log.
166        error!("Failed to send leave room presence: {}", e);
167    }
168
169    agent.rooms_leaving.insert(room, nickname.clone());
170}
171
172#[derive(Clone, Debug)]
173pub struct RoomMessageSettings<'a> {
174    pub room: BareJid,
175    pub message: &'a str,
176    pub lang: Option<&'a str>,
177}
178
179impl<'a> RoomMessageSettings<'a> {
180    pub fn new(room: BareJid, message: &'a str) -> Self {
181        Self {
182            room,
183            message,
184            lang: None,
185        }
186    }
187
188    pub fn with_lang(mut self, lang: &'a str) -> Self {
189        self.lang = Some(lang);
190        self
191    }
192}
193
194pub async fn send_room_message<'a>(agent: &mut Agent, settings: RoomMessageSettings<'a>) {
195    let RoomMessageSettings {
196        room,
197        message,
198        lang,
199    } = settings;
200
201    // TODO: check that room is in agent.joined_rooms
202    agent
203        .send_raw_message(
204            RawMessageSettings::new(room.into(), MessageType::Groupchat, message)
205                .with_lang_option(lang),
206        )
207        .await;
208}