Compare commits

...
Author SHA1 Message Date
95b9526912 feat: vendored-openssl feature for xmpp crate 2026-06-23 17:09:29 +02:00
Jonas Schäfer
1710a6f99e Release all crates
Since we tie the versions rather tightly, we have to release the entire
bunch in order to allow people to upgrade to rxml 0.14.
2026-06-11 17:25:32 +02:00
Jonas Schäfer
a90d9fafb6 xso: fix build on no_std 2026-06-11 17:20:39 +02:00
Link Mauve
d0b28a94b0 Implement XEP-0463: MUC Affiliations Versioning 2026-06-11 13:23:18 +00:00
Link Mauve
59e803bd7e xmpp: Make bound_jid a FullJid 2026-06-11 15:07:31 +02:00
Link Mauve
d848b6efe7 tokio-xmpp: Make bound_jid a FullJid
That way we have fewer checks to make in client code.
2026-06-11 15:07:31 +02:00
Link Mauve
47c0edc3f6 xmpp-parsers: Optimize to_hex() a bit
Just one correctly-sized String allocation, instead of one String per
byte, a Vec<String>, and then a final String.

skip-changelog: Internal change.
2026-06-11 14:53:35 +02:00
Jonas Schäfer
09640de5f2 tokio-xmpp: save bound_jid and features in ClientWorker
This fixes a regression from 10f1b3663c
reported by @full-bars in a discussion on an unrelated issue [1].

skip-changelog, because the regression has not been released yet.

   [1]: https://gitlab.com/xmpp-rs/xmpp-rs/-/merge_requests/675#note_3445588227
2026-06-11 14:26:51 +02:00
abd9f7d136 feat: Component.jid is now a BareJid 2026-06-11 08:44:44 +02:00
O01eg
24ae2ffd73 xmpp-parsers: Add 'approved' attribute to roster item 2026-06-10 21:24:24 +00:00
26 changed files with 166 additions and 43 deletions

View file

@ -1,4 +1,6 @@
Version NEXT, release XXXX-XX-XX:
Version 0.12.3, release 2026-06-11:
* Changes:
- Add a `DomainRef::with_node_str()` method, matching `with_resource_str()`
but for `with_node()`.

View file

@ -1,6 +1,6 @@
[package]
name = "jid"
version = "0.12.2"
version = "0.12.3"
authors = [
"lumi <lumi@pew.im>",
"Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>",

View file

@ -1,6 +1,6 @@
[package]
name = "xmpp-parsers"
version = "0.22.0"
version = "0.23.0"
authors = [
"Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>",
"Maxime “pep” Buquet <pep@bouah.net>",
@ -25,7 +25,7 @@ log = { version = "0.4", optional = true }
# same repository dependencies
jid = { version = "0.12", path = "../jid", features = ["minidom"] }
minidom = { version = "0.19", path = "../minidom" }
xso = { version = "0.3", path = "../xso", features = ["macros", "minidom", "panicking-into-impl", "jid", "uuid", "base64", "serde_json"] }
xso = { version = "0.4", path = "../xso", features = ["macros", "minidom", "panicking-into-impl", "jid", "uuid", "base64", "serde_json"] }
uuid = { version = "1.9.1", features = ["v4"] }
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }

View file

@ -1,6 +1,10 @@
Version NEXT:
XXXX-YY-ZZ RELEASER <admin@example.com>
XXXX-YY-ZZ RELEASER <releaser@domain.example>
Version 0.23.0:
2026-06-11 Jonas Schäfer <jonas@zombofant.net>
* New parsers/serialisers:
- MUC Affiliations Versioning (XEP-0463) (!668)
- Client Access Management (XEP-0494) (!641)
- Jingle Content Category (XEP-0507)
- Displayed markers (XEP-0333) (!669)
@ -33,6 +37,7 @@ XXXX-YY-ZZ RELEASER <admin@example.com>
- jingle_rtp::Description gained a bandwidth child.
- Fix cargo test for the disable-validation and component features.
- Structs of the bind module now have accessible inner pieces (!675).
- Add 'approved' field to roster item (!683).
Version 0.22.0:
2025-10-28 pep <pep@bouah.net>

View file

@ -690,6 +690,14 @@
<xmpp:since>0.20.0</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0463.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.2.0</xmpp:version>
<xmpp:since>NEXT</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0478.html"/>

View file

@ -6,6 +6,7 @@
use alloc::borrow::Cow;
use core::{
fmt::Write,
num::ParseIntError,
ops::{Deref, DerefMut},
str::FromStr,
@ -181,11 +182,11 @@ impl Hash {
/// Formats this hash into hexadecimal.
pub fn to_hex(&self) -> String {
self.hash
.iter()
.map(|byte| format!("{:02x}", byte))
.collect::<Vec<_>>()
.join("")
let mut hex = String::with_capacity(self.hash.len() * 2);
for byte in &self.hash {
write!(&mut hex, "{:02x}", byte).unwrap();
}
hex
}
/// Formats this hash into colon-separated hexadecimal.

69
parsers/src/muc/mav.rs Normal file
View file

@ -0,0 +1,69 @@
// Copyright (c) 2026 Link Mauve <linkmauve@linkmauve.fr>
//
// This Source Code Form is subject to the terms of the Mozilla Public
// 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/.
use xso::{AsXml, FromXml};
use crate::ns;
generate_id!(
/// Unique and opaque string, indicating the last affiliation version sent by the server that
/// the client has seen, and cached.
Version
);
/// A `<mav/>` element sent by the client on join, if it wants to keep track of the affiliations in
/// the room.
#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
#[xml(namespace = ns::MAV, name = "mav")]
pub struct AffiliationsVersioning {
/// Unique and opaque string, indicating the last affiliation version sent by the server that
/// the client has seen, and cached. Sending the mav element without a since attribute is a
/// called bootstrap request, which asks the server for a full response.
#[xml(attribute(default))]
since: Option<Version>,
/// The latest version the server has.
#[xml(attribute(default))]
until: Option<Version>,
}
#[cfg(test)]
mod tests {
use super::*;
use minidom::Element;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Version, 12);
assert_size!(AffiliationsVersioning, 24);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Version, 24);
assert_size!(AffiliationsVersioning, 48);
}
#[test]
fn test_simple() {
let elem: Element = "<mav xmlns='urn:xmpp:muc:affiliations:1'/>"
.parse()
.unwrap();
let ver = AffiliationsVersioning::try_from(elem).unwrap();
assert!(ver.since.is_none());
assert!(ver.until.is_none());
let elem: Element =
"<mav xmlns='urn:xmpp:muc:affiliations:1' since='9pacabr2q1' until='ruz41312vw'/>"
.parse()
.unwrap();
let ver = AffiliationsVersioning::try_from(elem).unwrap();
assert_eq!(ver.since.unwrap().0, "9pacabr2q1");
assert_eq!(ver.until.unwrap().0, "ruz41312vw");
}
}

View file

@ -11,5 +11,8 @@ pub mod muc;
/// The `http://jabber.org/protocol/muc#user` protocol.
pub mod user;
/// The `urn:xmpp:muc:affiliations:1` protocol.
pub mod mav;
pub use self::muc::Muc;
pub use self::user::MucUser;

View file

@ -8,6 +8,7 @@
use xso::{AsXml, FromXml};
use crate::message::MessagePayload;
use crate::muc::mav::AffiliationsVersioning;
use crate::ns;
use crate::presence::PresencePayload;
@ -302,6 +303,10 @@ pub struct MucUser {
/// A mediated invite rejection
#[xml(child(default))]
pub decline: Option<Decline>,
/// From XEP-0463: MUC Affiliations Versioning
#[xml(child(default))]
pub affiliations: Option<AffiliationsVersioning>,
}
impl MucUser {
@ -347,7 +352,7 @@ mod tests {
assert_size!(Item, 84);
assert_size!(Invite, 44);
assert_size!(Decline, 44);
assert_size!(MucUser, 112);
assert_size!(MucUser, 136);
}
#[cfg(target_pointer_width = "64")]
@ -362,7 +367,7 @@ mod tests {
assert_size!(Item, 168);
assert_size!(Invite, 88);
assert_size!(Decline, 88);
assert_size!(MucUser, 224);
assert_size!(MucUser, 272);
}
#[test]

View file

@ -304,6 +304,9 @@ pub const SASL_CB: &str = "urn:xmpp:sasl-cb:0";
/// XEP-0444: Message Reactions
pub const REACTIONS: &str = "urn:xmpp:reactions:0";
/// XEP-0463: MUC Affiliations Versioning
pub const MAV: &str = "urn:xmpp:muc:affiliations:1";
/// XEP-0478: Stream Limits Advertisement
pub const STREAM_LIMITS: &str = "urn:xmpp:stream-limits:0";

View file

@ -71,6 +71,10 @@ pub struct Item {
/// Groups this contact is part of.
#[xml(child(n = ..))]
pub groups: Vec<Group>,
/// Subscription pre-approval
#[xml(attribute(default))]
pub approved: Option<bool>,
}
/// The contact list of the user.
@ -158,6 +162,7 @@ mod tests {
<item jid='contact@example.org'
subscription='none'
ask='subscribe'
approved='true'
name='MyContact'>
<group>MyBuddies</group>
</item>
@ -175,6 +180,7 @@ mod tests {
assert_eq!(roster.items[0].name, Some(String::from("Romeo")));
assert_eq!(roster.items[0].subscription, Subscription::Both);
assert_eq!(roster.items[0].ask, Ask::None);
assert_eq!(roster.items[0].approved, None);
assert_eq!(
roster.items[0].groups,
vec!(Group::from_str("Friends").unwrap())
@ -187,6 +193,7 @@ mod tests {
assert_eq!(roster.items[3].name, Some(String::from("MyContact")));
assert_eq!(roster.items[3].subscription, Subscription::None);
assert_eq!(roster.items[3].ask, Ask::Subscribe);
assert_eq!(roster.items[3].approved, Some(true));
assert_eq!(
roster.items[3].groups,
vec!(Group::from_str("MyBuddies").unwrap())

View file

@ -1,6 +1,6 @@
[package]
name = "tokio-xmpp"
version = "5.0.0"
version = "6.0.0"
authors = ["Astro <astro@spaceboyz.net>", "Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>", "pep <pep+code@bouah.net>", "O01eg <o01eg@yandex.ru>", "SonnyX <randy@vonderweide.nl>", "Paul Fariello <paul@fariello.eu>"]
description = "Asynchronous XMPP for Rust with tokio"
license = "MPL-2.0"
@ -26,8 +26,8 @@ pin-project-lite = { version = "0.2" }
thiserror = "2.0"
# same repository dependencies
sasl = { version = "0.5", path = "../sasl" }
xmpp-parsers = { version = "0.22", path = "../parsers", features = [ "log" ] }
xso = { version = "0.3", path = "../xso" }
xmpp-parsers = { version = "0.23", path = "../parsers", features = [ "log" ] }
xso = { version = "0.4", path = "../xso" }
# these are only needed for starttls ServerConnector support
hickory-resolver = { version = "0.26", optional = true}

View file

@ -1,13 +1,17 @@
Version NEXT:
0000-00-00 RELEASER <releaser@domain>
XXXX-XX-XX RELEASER <releaser@domain.example>
Version 6.0.0:
2026-06-10 Jonas Schäfer <jonas@zombofant.net>
* Breaking:
- Removed the `disable-validation` feature and made the behaviour the
new default. The newly-introduced `pedantic` feature can be used to
opt into the previous default behaviour of rejecting all unknown
child elements and attributes.
* Breaking:
- Add `xmpp_parsers::stream_features::StreamFeatures` to
`tokio_xmpp::event::Online` (!631)
`tokio_xmpp::event::Online` (!631)
- `Component.jid` is now a `BareJid` and not a raw `Jid` (!684)
- `Client.bound_jid` is now a `FullJid` and not a raw `Jid` (!687)
* Added:
- Expose `client_auth` method to allow manual stream setups for advanced
use cases.
@ -28,10 +32,9 @@ Version NEXT:
- Drive the XMPP client stream in the background (!631)
- Add option to split XMPP client into read and write half (!631)
- Remove T: AsXml bound from XmlStream (!675)
- Invalid stanzas are now handled dropped (after increasing the SM
counters) instead of failing the stream. Previously, that might have
caused continuous reconnect loops esp. with stream management (#172,
!675).
- Invalid stanzas are now dropped (after increasing the SM counters)
instead of failing the stream. Previously, that might have caused
continuous reconnect loops esp. with stream management (#172, !675).
Version 5.0.0:
2025-10-28 pep <pep@bouah.net>

View file

@ -15,7 +15,10 @@ use std::io;
use std::sync::Arc;
use tokio::sync::{mpsc, oneshot, Mutex};
use tokio::task::JoinHandle;
use xmpp_parsers::{jid::Jid, stream_features::StreamFeatures};
use xmpp_parsers::{
jid::{FullJid, Jid},
stream_features::StreamFeatures,
};
#[cfg(feature = "direct-tls")]
use crate::connect::DirectTlsServerConnector;
@ -54,7 +57,7 @@ pub struct Client {
// Client worker task
worker: JoinHandle<stanzastream::StanzaReceiver>,
// JID of the logged-in client
bound_jid: Option<Jid>,
bound_jid: Option<FullJid>,
// Stream features of the currently connected stream
features: Option<StreamFeatures>,
// Response tracker for IQs
@ -64,7 +67,7 @@ pub struct Client {
impl Client {
/// Get the client's bound JID (the one reported by the XMPP
/// server).
pub fn bound_jid(&self) -> Option<&Jid> {
pub fn bound_jid(&self) -> Option<&FullJid> {
self.bound_jid.as_ref()
}

View file

@ -11,7 +11,7 @@ use futures::StreamExt;
use futures::{task::Poll, Stream};
use std::sync::Arc;
use tokio::sync::Mutex;
use xmpp_parsers::{jid::Jid, stream_features::StreamFeatures};
use xmpp_parsers::{jid::FullJid, stream_features::StreamFeatures};
/// Read half of a [`Client`](crate::Client).
#[derive(Debug)]
@ -22,7 +22,7 @@ impl ClientReceiver {
///
/// See the documentation of [`Client::bound_jid`](crate::Client::bound_jid) for more
/// information.
pub async fn bound_jid(&self) -> Option<Jid> {
pub async fn bound_jid(&self) -> Option<FullJid> {
self.0.lock().await.bound_jid.clone()
}

View file

@ -36,7 +36,8 @@ impl Stream for Client {
..
} = event
{
self.bound_jid = Some(bound_jid.clone());
// This unwrap() will never fail because the server MUST send us a full JID.
self.bound_jid = Some(bound_jid.try_as_full().unwrap().clone());
self.features = Some(features.clone());
}

View file

@ -12,7 +12,7 @@ use core::ops::ControlFlow;
use futures::StreamExt;
use tokio::sync::mpsc;
use tokio::sync::oneshot;
use xmpp_parsers::jid::Jid;
use xmpp_parsers::jid::FullJid;
use xmpp_parsers::stream_features::StreamFeatures;
/// Worker to drive the [`crate::stanzastream`] of a client in the background and continue to
@ -25,7 +25,7 @@ pub struct ClientWorker {
// Shutdown signal receiver from frontend
shutdown_rx: oneshot::Receiver<()>,
// JID of the logged-in client
bound_jid: Option<Jid>,
bound_jid: Option<FullJid>,
// Stream features of the currently connected stream
features: Option<StreamFeatures>,
// Response tracker for IQs
@ -81,6 +81,9 @@ impl ClientWorker {
bound_jid,
features,
}) => {
// This unwrap() will never fail, because the server always uses our own bound JID.
self.bound_jid = Some(bound_jid.try_as_full().unwrap().clone());
self.features = Some(features.clone());
self.iq_response_tracker
.set_account_jid(bound_jid.to_bare());
@ -91,7 +94,7 @@ impl ClientWorker {
}
}
StanzaStreamEvent::Stream(StreamEvent::Resumed) => Event::Online {
bound_jid: self.bound_jid.as_ref().unwrap().clone(),
bound_jid: self.bound_jid.as_ref().unwrap().clone().into(),
features: self.features.as_ref().unwrap().clone(),
resumed: true,
},

View file

@ -2,7 +2,7 @@ use std::io;
use futures::{SinkExt, StreamExt};
use tokio::io::{AsyncBufRead, AsyncWrite};
use xmpp_parsers::{component::Handshake, jid::Jid, ns};
use xmpp_parsers::{component::Handshake, jid::BareJid, ns};
use crate::component::ServerConnector;
use crate::error::{AuthError, Error};
@ -11,7 +11,7 @@ use crate::xmlstream::{ReadError, Timeouts, XmppStream, XmppStreamElement};
/// Log into an XMPP server as a client with a jid+pass
pub async fn component_login<C: ServerConnector>(
connector: C,
jid: Jid,
jid: BareJid,
password: &str,
timeouts: Timeouts,
) -> Result<XmppStream<C::Stream>, Error> {

View file

@ -3,7 +3,7 @@
//! allowed to use any user and resource identifiers in their stanzas.
use futures::sink::SinkExt;
use std::str::FromStr;
use xmpp_parsers::jid::Jid;
use xmpp_parsers::jid::BareJid;
use crate::{
component::login::component_login,
@ -27,7 +27,7 @@ mod stream;
/// (stanzas). Connection handling however is up to the user.
pub struct Component<C: ServerConnector> {
/// The component's Jabber-Id
pub jid: Jid,
pub jid: BareJid,
stream: XmppStream<C::Stream>,
}
@ -87,7 +87,7 @@ impl<C: ServerConnector> Component<C> {
connector: C,
timeouts: Timeouts,
) -> Result<Self, Error> {
let jid = Jid::from_str(jid)?;
let jid = BareJid::from_str(jid)?;
let stream = component_login(connector, jid.clone(), password, timeouts).await?;
Ok(Component { jid, stream })
}

View file

@ -1,6 +1,6 @@
[package]
name = "xmpp"
version = "0.6.0"
version = "0.7.0"
authors = [
"Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>",
"Maxime “pep” Buquet <pep@bouah.net>",
@ -21,7 +21,7 @@ log = "0.4"
reqwest = { version = "0.13", features = ["stream"], default-features = false }
tokio-util = { version = "0.7", features = ["codec"] }
# same repository dependencies
tokio-xmpp = { version = "5.0", path = "../tokio-xmpp", default-features = false }
tokio-xmpp = { version = "6.0", path = "../tokio-xmpp", default-features = false }
[dev-dependencies]
env_logger = { version = "0.11", default-features = false, features = ["auto-color", "humantime"] }
@ -50,3 +50,5 @@ escape-hatch = []
syntax-highlighting = [ "tokio-xmpp/syntax-highlighting" ]
# Enable serde support in jid crate
serde = [ "tokio-xmpp/serde" ]
vendored-openssl = [ "reqwest/native-tls-vendored" ]

View file

@ -1,5 +1,8 @@
Version NEXT
XXXX-YY-ZZ [ RELEASER <admin@localhost> ]
XXXX-XX-XX [ RELEASER <releaser@domain.example> ]
Version 0.7.0
2026-06-10 [ Jonas Schäfer <jonas@zombofant.net> ]
* Breaking:
- Agent::wait_for_events now return Vec<Event> and sets inner tokio_xmpp Client
auto-reconnect to true... It is still aware of Event::Disconnected but should
@ -28,6 +31,7 @@ XXXX-YY-ZZ [ RELEASER <admin@localhost> ]
Please refer to the crate docs for details. (!581)
- Allow joining an already joined room with `Agent::join_room` to enable
resyncs. Adds a parameter to `muc::room::JoinRoomSettings`.
- Make bound_jid a FullJid. (!687)
* Changed:
- Replaced unimplemented! with info! calls.
- Only send `Event::RoomSubject` when there's no body as per 0045.

View file

@ -13,7 +13,7 @@ use tokio::sync::RwLock;
use crate::{
Config, Error, Event, RoomNick, event_loop,
jid::{BareJid, Jid},
jid::{BareJid, FullJid, Jid},
message, muc,
parsers::disco::DiscoInfoResult,
upload,
@ -111,7 +111,7 @@ impl Agent {
/// Get the bound jid of the client.
///
/// If the client is not connected, this will be None.
pub fn bound_jid(&self) -> Option<&Jid> {
pub fn bound_jid(&self) -> Option<&FullJid> {
self.client.bound_jid()
}
}

View file

@ -1,6 +1,6 @@
[package]
name = "xso_proc"
version = "0.2.0"
version = "0.3.0"
authors = [
"Jonas Schäfer <jonas@zombofant.net>",
]

View file

@ -1,6 +1,6 @@
[package]
name = "xso"
version = "0.3.0"
version = "0.4.0"
edition = "2021"
description = "XML Streamed Objects: similar to serde, but XML-native."
homepage = "https://xmpp.rs"
@ -13,7 +13,7 @@ license = "MPL-2.0"
bytes = { version = "1" }
rxml = { version = "0.14.0", default-features = false }
minidom = { version = "0.19", path = "../minidom" }
xso_proc = { version = "0.2", path = "../xso-proc", optional = true }
xso_proc = { version = "0.3", path = "../xso-proc", optional = true }
# optional dependencies to provide text conversion to/from types from/using
# these crates

View file

@ -1,4 +1,6 @@
Version NEXT:
Version 0.4.0, release 2026-06-10:
* Breaking
- Removed the `non-pedantic` feature and made the behaviour the default.
- Added the `pedantic` feature, opting into the previous default
@ -6,6 +8,7 @@ Version NEXT:
* Changes
- Fix some Clippy warnings
- Fix build with minidom and without std (!661)
- Fix no_std build
* Added
- xso::from_bytes_with_options and xso::from_reader_with_options to allow
passing custom parser configuration.

View file

@ -702,6 +702,7 @@ fn read_start_event_io(
))
}
#[cfg(feature = "std")]
fn from_reader_inner<T: FromXml, R: io::BufRead>(
mut reader: rxml::XmlLangTracker<rxml::Reader<R>>,
) -> io::Result<T> {