1
0
mirror of https://gitlab.com/xmpp-rs/xmpp-rs.git synced 2024-06-26 17:08:26 +02:00

Merge branch 'message-id' into 'main'

Message IDs

See merge request xmpp-rs/xmpp-rs!256
This commit is contained in:
Werner Kroneman 2024-03-17 19:28:51 +00:00
commit e151cde1ac
11 changed files with 179 additions and 33 deletions

View File

@ -67,16 +67,18 @@ async fn main() {
.with_to(iq.from.unwrap()); .with_to(iq.from.unwrap());
client.send_stanza(iq.into()).await.unwrap(); client.send_stanza(iq.into()).await.unwrap();
} }
Err(err) => client Err(err) => {
.send_stanza(make_error( client
iq.from.unwrap(), .send_stanza(make_error(
iq.id, iq.from.unwrap(),
ErrorType::Modify, iq.id,
DefinedCondition::BadRequest, ErrorType::Modify,
&format!("{}", err), DefinedCondition::BadRequest,
)) &format!("{}", err),
.await ))
.unwrap(), .await
.unwrap();
}
} }
} else { } else {
// We MUST answer unhandled get iqs with a service-unavailable error. // We MUST answer unhandled get iqs with a service-unavailable error.

View File

@ -76,10 +76,16 @@ impl<C: ServerConnector> Client<C> {
} }
} }
/// Send stanza /// Send stanza. An id will be assigned if not already present.
pub async fn send_stanza(&mut self, stanza: Element) -> Result<(), Error> { pub async fn send_stanza(&mut self, stanza: Element) -> Result<String, Error> {
self.send(Packet::Stanza(add_stanza_id(stanza, ns::JABBER_CLIENT))) let stanza = add_stanza_id(stanza, ns::JABBER_CLIENT);
.await
// Get the Id. (With add_stanza_id, it should always be present)
let id = stanza.attr("id").unwrap().to_string();
self.send(Packet::Stanza(stanza)).await?;
Ok(id)
} }
/// Get the stream features (`<stream:features/>`) of the underlying stream /// Get the stream features (`<stream:features/>`) of the underlying stream

View File

@ -76,7 +76,8 @@ async fn main() -> Result<(), Option<()>> {
println!("Joined room {}.", jid); println!("Joined room {}.", jid);
client client
.send_message(Jid::Bare(jid), MessageType::Groupchat, "en", "Hello world!") .send_message(Jid::Bare(jid), MessageType::Groupchat, "en", "Hello world!")
.await; .await
.expect("Could not send message.");
} }
Event::RoomLeft(jid) => { Event::RoomLeft(jid) => {
println!("Left room {}.", jid); println!("Left room {}.", jid);

View File

@ -60,23 +60,52 @@ impl<C: ServerConnector> Agent<C> {
muc::room::leave_room(self, room_jid, nickname, lang, status).await muc::room::leave_room(self, room_jid, nickname, lang, status).await
} }
/// Send a message to a given recipient.
///
/// Returns the generated message ID.
///
/// # Arguments
///
/// * `recipient`: The JID of the recipient.
/// * `type_`: The type of message to send.
/// For a message to a MUC room, this should be `MessageType::Groupchat`.
/// For a private message to a MUC room member or regular contact,
/// this should be `MessageType::Chat`.
/// * `lang`: The language of the message. (See IETF RFC 5646)
/// * `text`: The text of the message.
pub async fn send_message( pub async fn send_message(
&mut self, &mut self,
recipient: Jid, recipient: Jid,
type_: MessageType, type_: MessageType,
lang: &str, lang: &str,
text: &str, text: &str,
) { ) -> Result<String, tokio_xmpp::Error> {
message::send::send_message(self, recipient, type_, lang, text).await message::send::send_message(self, recipient, type_, lang, text).await
} }
/// Send a private message to a given MUC room member.
///
/// Returns the generated message ID.
///
/// # Arguments
///
/// * `room`: The JID of the room.
/// * `recipient`: The nickname of the recipient within the room.
/// * `type_`: The type of message to send.
/// For a message to a MUC room, this should be `MessageType::Groupchat`.
/// For a private message to a MUC room member or regular contact,
/// this should be `MessageType::Chat`.
/// * `lang`: The language of the message. (See IETF RFC 5646)
/// * `text`: The text of the message.
///
/// Returns the generated message ID, or an error if the message could not be sent.
pub async fn send_room_private_message( pub async fn send_room_private_message(
&mut self, &mut self,
room: BareJid, room: BareJid,
recipient: RoomNick, recipient: RoomNick,
lang: &str, lang: &str,
text: &str, text: &str,
) { ) -> Result<String, tokio_xmpp::Error> {
muc::private_message::send_room_private_message(self, room, recipient, lang, text).await muc::private_message::send_room_private_message(self, room, recipient, lang, text).await
} }

View File

@ -8,6 +8,7 @@
use tokio_xmpp::parsers::Jid; use tokio_xmpp::parsers::Jid;
use tokio_xmpp::parsers::{bookmarks2, message::Body, roster::Item as RosterItem, BareJid}; use tokio_xmpp::parsers::{bookmarks2, message::Body, roster::Item as RosterItem, BareJid};
use crate::message::MucMessageId;
use crate::{delay::StanzaTimeInfo, Error, Id, RoomNick}; use crate::{delay::StanzaTimeInfo, Error, Id, RoomNick};
#[derive(Debug)] #[derive(Debug)]
@ -30,15 +31,42 @@ pub enum Event {
LeaveAllRooms, LeaveAllRooms,
RoomJoined(BareJid), RoomJoined(BareJid),
RoomLeft(BareJid), RoomLeft(BareJid),
RoomMessage(Id, BareJid, RoomNick, Body, StanzaTimeInfo), /// A message received from a group chat room.
/// - The [`MucMessageId`] is the ID of the message; it contains the ID assigned by the room,
/// and the ID assigned by the sending client.
/// - The [`BareJid`] is the room's address.
/// - The [`RoomNick`] is the nickname of the room member who sent the message.
/// - The [`Body`] is the message body.
/// - The [`StanzaTimeInfo`] about when message was received, and when the message was claimed sent.
///
/// Note: if the sender_assigned_id matches the ID returned by Agent::send_message,
/// then the message is an echo of a message sent by the client.
RoomMessage(MucMessageId, BareJid, RoomNick, Body, StanzaTimeInfo),
/// The subject of a room was received. /// The subject of a room was received.
/// - The BareJid is the room's address. /// - The MucMessageId is the ID of the message that contained the subject;
/// - The RoomNick is the nickname of the room member who set the subject. /// it contains the ID assigned by the room, and the ID assigned by the sending client.
/// - The String is the new subject. /// - The [`BareJid`] is the room's address.
RoomSubject(BareJid, Option<RoomNick>, String, StanzaTimeInfo), /// - The [`RoomNick`] is the nickname of the room member who set the subject.
/// A private message received from a room, containing the message ID, the room's BareJid, /// - The [`String`] is the new subject.
/// the sender's nickname, and the message body. /// - The [`StanzaTimeInfo`] about when message was received, and when the message was claimed sent.
RoomPrivateMessage(Id, BareJid, RoomNick, Body, StanzaTimeInfo), RoomSubject(
MucMessageId,
BareJid,
Option<RoomNick>,
String,
StanzaTimeInfo,
),
/// A private message received from a member of a room.
/// - The [`MucMessageId`] is the ID of the message; it contains the ID assigned by the room,
/// and the ID assigned by the sending client.
/// - The [`BareJid`] is the room's address.
/// - The [`RoomNick`] is the nickname of the room member who sent the message.
/// - The [`Body`] is the message body.
/// - The [`StanzaTimeInfo`] about when message was received, and when the message was claimed sent.
///
/// Note: if the sender_assigned_id matches the ID returned by Agent::send_room_private_message,
/// then the message is an echo of a message sent by the client.
RoomPrivateMessage(MucMessageId, BareJid, RoomNick, Body, StanzaTimeInfo),
ServiceMessage(Id, BareJid, Body, StanzaTimeInfo), ServiceMessage(Id, BareJid, Body, StanzaTimeInfo),
HttpUploadedFile(String), HttpUploadedFile(String),
} }

View File

@ -33,6 +33,7 @@ pub use feature::ClientFeature;
pub type Error = tokio_xmpp::Error; pub type Error = tokio_xmpp::Error;
pub type Id = Option<String>; pub type Id = Option<String>;
pub type RoomNick = String; pub type RoomNick = String;
#[cfg(all(test, any(feature = "starttls-rust", feature = "starttls-native")))] #[cfg(all(test, any(feature = "starttls-rust", feature = "starttls-native")))]

View File

@ -4,5 +4,24 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this // License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/. // file, You can obtain one at http://mozilla.org/MPL/2.0/.
use crate::Id;
pub mod receive; pub mod receive;
pub mod send; pub mod send;
/// A message ID for a message in a MUC room.
///
/// Note: this struct is a bit weird due to an inconsistency between XEPs 0308 and 0424.
///
/// XEP-0424 (Message Retraction) designates messages to correct by the sender-assigned ID,
/// but XEP-0308 (Last Message Correction) designates messages to correct by the room-assigned ID.
///
/// Hence, both are needed, but depending on context, one or the other may be missing or may
/// refer to something else; see the context-specific documentation for more information.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct MucMessageId {
/// The ID of the message as assigned by the client that sent it.
pub sender_assigned_id: Id,
/// The ID of the message as assigned by the room.
pub room_assigned_id: Id,
}

View File

@ -10,6 +10,7 @@ use tokio_xmpp::{
Jid, Jid,
}; };
use crate::message::receive::group_chat::extract_muc_message_id;
use crate::{delay::StanzaTimeInfo, Agent, Event}; use crate::{delay::StanzaTimeInfo, Agent, Event};
pub async fn handle_message_chat<C: ServerConnector>( pub async fn handle_message_chat<C: ServerConnector>(
@ -37,7 +38,7 @@ pub async fn handle_message_chat<C: ServerConnector>(
) )
} }
Jid::Full(full) => Event::RoomPrivateMessage( Jid::Full(full) => Event::RoomPrivateMessage(
message.id.clone(), extract_muc_message_id(message),
full.to_bare(), full.to_bare(),
full.resource().to_string(), full.resource().to_string(),
body.clone(), body.clone(),

View File

@ -7,6 +7,9 @@
use tokio_xmpp::connect::ServerConnector; use tokio_xmpp::connect::ServerConnector;
use tokio_xmpp::{parsers::message::Message, Jid}; use tokio_xmpp::{parsers::message::Message, Jid};
use crate::message::MucMessageId;
use crate::parsers::ns::SID;
use crate::parsers::stanza_id::StanzaId;
use crate::{delay::StanzaTimeInfo, Agent, Event}; use crate::{delay::StanzaTimeInfo, Agent, Event};
pub async fn handle_message_group_chat<C: ServerConnector>( pub async fn handle_message_group_chat<C: ServerConnector>(
@ -18,8 +21,11 @@ pub async fn handle_message_group_chat<C: ServerConnector>(
) { ) {
let langs: Vec<&str> = agent.lang.iter().map(String::as_str).collect(); let langs: Vec<&str> = agent.lang.iter().map(String::as_str).collect();
let id = extract_muc_message_id(message);
if let Some((_lang, subject)) = message.get_best_subject(langs.clone()) { if let Some((_lang, subject)) = message.get_best_subject(langs.clone()) {
events.push(Event::RoomSubject( events.push(Event::RoomSubject(
id.clone(),
from.to_bare(), from.to_bare(),
from.resource().map(|x| x.to_string()), from.resource().map(|x| x.to_string()),
subject.0.clone(), subject.0.clone(),
@ -30,7 +36,7 @@ pub async fn handle_message_group_chat<C: ServerConnector>(
if let Some((_lang, body)) = message.get_best_body(langs) { if let Some((_lang, body)) = message.get_best_body(langs) {
let event = match from.clone() { let event = match from.clone() {
Jid::Full(full) => Event::RoomMessage( Jid::Full(full) => Event::RoomMessage(
message.id.clone(), id,
from.to_bare(), from.to_bare(),
full.resource().to_string(), full.resource().to_string(),
body.clone(), body.clone(),
@ -43,3 +49,23 @@ pub async fn handle_message_group_chat<C: ServerConnector>(
events.push(event) events.push(event)
} }
} }
/// Extract the sender-assigned and room-assigned IDs from a message.
pub fn extract_muc_message_id(message: &Message) -> MucMessageId {
// Find a stanza-id element, if any.
let stanza_id = message
.payloads
.iter()
.find(|payload| payload.is("stanza-id", SID))
.map(|elem| {
StanzaId::try_from(elem.clone())
.expect("Failed to parse stanza-id")
.id
});
let id = MucMessageId {
sender_assigned_id: message.id.clone(),
room_assigned_id: stanza_id,
};
id
}

View File

@ -7,22 +7,38 @@
use tokio_xmpp::connect::ServerConnector; use tokio_xmpp::connect::ServerConnector;
use tokio_xmpp::{ use tokio_xmpp::{
parsers::message::{Body, Message, MessageType}, parsers::message::{Body, Message, MessageType},
Jid, Error, Jid,
}; };
use crate::Agent; use crate::Agent;
/// Send a message to a given recipient.
///
/// Returns the generated message ID.
///
/// # Arguments
///
/// * `agent`: The agent to use to send the message.
/// * `recipient`: The JID of the recipient.
/// * `type_`: The type of message to send.
/// For a message to a MUC room, this should be `MessageType::Groupchat`.
/// For a private message to a MUC room member or regular contact,
/// this should be `MessageType::Chat`.
/// * `lang`: The language of the message. (See IETF RFC 5646)
/// * `text`: The text of the message.
///
/// Returns the generated message ID, or an error if the message could not be sent.
pub async fn send_message<C: ServerConnector>( pub async fn send_message<C: ServerConnector>(
agent: &mut Agent<C>, agent: &mut Agent<C>,
recipient: Jid, recipient: Jid,
type_: MessageType, type_: MessageType,
lang: &str, lang: &str,
text: &str, text: &str,
) { ) -> Result<String, Error> {
let mut message = Message::new(Some(recipient)); let mut message = Message::new(Some(recipient));
message.type_ = type_; message.type_ = type_;
message message
.bodies .bodies
.insert(String::from(lang), Body(String::from(text))); .insert(String::from(lang), Body(String::from(text)));
let _ = agent.client.send_stanza(message.into()).await; agent.client.send_stanza(message.into()).await
} }

View File

@ -10,23 +10,40 @@ use tokio_xmpp::{
message::{Body, Message, MessageType}, message::{Body, Message, MessageType},
muc::user::MucUser, muc::user::MucUser,
}, },
BareJid, Jid, BareJid, Error, Jid,
}; };
use crate::{Agent, RoomNick}; use crate::{Agent, RoomNick};
/// Send a private message to a given MUC room member.
///
/// Returns the generated message ID.
///
/// # Arguments
///
/// * `agent`: The agent to use to send the message.
/// * `room`: The JID of the room.
/// * `recipient`: The nickname of the recipient within the room.
/// * `type_`: The type of message to send.
/// For a message to a MUC room, this should be `MessageType::Groupchat`.
/// For a private message to a MUC room member or regular contact,
/// this should be `MessageType::Chat`.
/// * `lang`: The language of the message. (See IETF RFC 5646)
/// * `text`: The text of the message.
///
/// Returns the generated message ID, or an error if the message could not be sent.
pub async fn send_room_private_message<C: ServerConnector>( pub async fn send_room_private_message<C: ServerConnector>(
agent: &mut Agent<C>, agent: &mut Agent<C>,
room: BareJid, room: BareJid,
recipient: RoomNick, recipient: RoomNick,
lang: &str, lang: &str,
text: &str, text: &str,
) { ) -> Result<String, Error> {
let recipient: Jid = room.with_resource_str(&recipient).unwrap().into(); let recipient: Jid = room.with_resource_str(&recipient).unwrap().into();
let mut message = Message::new(recipient).with_payload(MucUser::new()); let mut message = Message::new(recipient).with_payload(MucUser::new());
message.type_ = MessageType::Chat; message.type_ = MessageType::Chat;
message message
.bodies .bodies
.insert(String::from(lang), Body(String::from(text))); .insert(String::from(lang), Body(String::from(text)));
let _ = agent.client.send_stanza(message.into()).await; agent.client.send_stanza(message.into()).await
} }