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