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}