mirror of
https://gitlab.com/xmpp-rs/xmpp-rs.git
synced 2024-07-12 22:21:53 +00:00
Introduce a typed Action API
This commit is contained in:
parent
76b68e932a
commit
47092e6b9a
10 changed files with 543 additions and 0 deletions
|
@ -21,10 +21,18 @@ tokio = { version = "1", features = ["fs"] }
|
|||
log = "0.4"
|
||||
reqwest = { version = "0.11.8", features = ["stream"] }
|
||||
tokio-util = { version = "0.7", features = ["codec"] }
|
||||
# For actions API
|
||||
async-trait = { version = "0.1", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.10"
|
||||
xmpp = { path = ".", features = [ "actions" ] }
|
||||
|
||||
[features]
|
||||
default = ["avatars"]
|
||||
avatars = []
|
||||
actions = [ "dep:async-trait" ]
|
||||
|
||||
[[example]]
|
||||
name = "actions_bot"
|
||||
required-features = [ "actions" ]
|
|
@ -6,6 +6,7 @@ xxxxxxxxxx
|
|||
* Improvements:
|
||||
- Agent is now Send, by replacing Rc with Arc and RefCell with RwLock (#64)
|
||||
- ClientBuilder now has a set_resource method for manual resource management (#72)
|
||||
- the action module is enabled by the `actions` feature (#75) and providers a more robust and ergonomic API
|
||||
|
||||
Version 0.4.0:
|
||||
2023-05-18 [ Maxime “pep” Buquet <pep@bouah.net>, Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> ]
|
||||
|
|
143
xmpp/examples/actions_bot.rs
Normal file
143
xmpp/examples/actions_bot.rs
Normal file
|
@ -0,0 +1,143 @@
|
|||
use env_logger;
|
||||
use std::str::FromStr;
|
||||
use xmpp::{action, ClientBuilder, ClientFeature, ClientType, Event};
|
||||
use xmpp_parsers::{BareJid, JidParseError};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Error {
|
||||
Syntax(String),
|
||||
Jid(String, JidParseError),
|
||||
XMPP(tokio_xmpp::Error),
|
||||
}
|
||||
|
||||
impl From<tokio_xmpp::Error> for Error {
|
||||
fn from(e: tokio_xmpp::Error) -> Error {
|
||||
Error::XMPP(e)
|
||||
}
|
||||
}
|
||||
|
||||
struct Args {
|
||||
jid: BareJid,
|
||||
password: String,
|
||||
room: BareJid,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
fn from_cli() -> Result<Option<Args>, Error> {
|
||||
let mut args = std::env::args();
|
||||
let binary_name = args.next().unwrap();
|
||||
|
||||
let jid = args
|
||||
.next()
|
||||
.ok_or(Error::Syntax("Missing argument: jid".into()))?;
|
||||
match jid.to_lowercase().as_str() {
|
||||
"help" | "-h" | "--help" => {
|
||||
help(&binary_name);
|
||||
return Ok(None);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let jid = BareJid::from_str(&jid).map_err(|e| Error::Jid(jid, e))?;
|
||||
|
||||
let password = args
|
||||
.next()
|
||||
.ok_or(Error::Syntax("Missing argument: password".into()))?;
|
||||
|
||||
let room = args
|
||||
.next()
|
||||
.ok_or(Error::Syntax("Missing argument: room".into()))?;
|
||||
let room = BareJid::from_str(&room).map_err(|e| Error::Jid(room, e))?;
|
||||
|
||||
Ok(Some(Args {
|
||||
jid,
|
||||
password,
|
||||
room,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn help(name: &str) {
|
||||
println!("A bot that welcomes you when you say hello");
|
||||
println!("Usage: {} <jid> <password> <room>", name);
|
||||
println!("NOTE: Run with RUST_LOG=debug for debug logs");
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Error> {
|
||||
env_logger::init();
|
||||
|
||||
let args = if let Some(args) = Args::from_cli()? {
|
||||
args
|
||||
} else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let nick = String::from("actions_bot");
|
||||
|
||||
// Client instance
|
||||
let mut client = ClientBuilder::new(args.jid.clone(), &args.password)
|
||||
.set_client(ClientType::Bot, "xmpp-rs")
|
||||
.set_website("https://gitlab.com/xmpp-rs/xmpp-rs")
|
||||
.set_default_nick(&nick)
|
||||
.enable_feature(ClientFeature::JoinRooms)
|
||||
.build();
|
||||
|
||||
while let Some(events) = client.wait_for_events().await {
|
||||
for event in events {
|
||||
match event {
|
||||
Event::Online => {
|
||||
println!("Online.");
|
||||
client
|
||||
.action(action::room_join(args.room.clone(), &nick))
|
||||
.await?;
|
||||
}
|
||||
Event::Disconnected => {
|
||||
println!("Disconnected");
|
||||
}
|
||||
Event::RoomMessage(_id, room_jid, author, body) => {
|
||||
println!(
|
||||
"Message in room {} from {}: {}",
|
||||
&room_jid, &author, &body.0
|
||||
);
|
||||
let msg = body.0.trim().to_lowercase();
|
||||
if msg == "hello" {
|
||||
client
|
||||
.action(action::room_message(
|
||||
room_jid,
|
||||
format!("Hello, {}", &author),
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Event::RoomPrivateMessage(_id, room_jid, nick, body) => {
|
||||
println!(
|
||||
"Private Message from room {} from {}: {}",
|
||||
&room_jid, &nick, body.0
|
||||
);
|
||||
client
|
||||
.action(action::room_private_message(
|
||||
room_jid,
|
||||
nick,
|
||||
format!("You said: {}", body.0),
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
Event::ChatMessage(_id, jid, body) => {
|
||||
println!("Message from {}: {}", jid, body.0);
|
||||
client
|
||||
.action(action::message(jid, format!("You said: {}", body.0)))
|
||||
.await?;
|
||||
}
|
||||
Event::RoomJoined(jid) => {
|
||||
println!("Joined room {}.", jid);
|
||||
client
|
||||
.action(action::room_message(jid, "Hello, world!"))
|
||||
.await?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
57
xmpp/src/action/message.rs
Normal file
57
xmpp/src/action/message.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
use crate::{Act, Agent, Error};
|
||||
use std::str::FromStr;
|
||||
use xmpp_parsers::{message::Message, Element, Jid, JidParseError};
|
||||
|
||||
pub struct MessageAction {
|
||||
pub jid: Jid,
|
||||
pub msg: String,
|
||||
pub lang: String,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Act<()> for MessageAction {
|
||||
async fn act(self, agent: &mut Agent) -> Result<(), Error> {
|
||||
agent.client.send_stanza(self.into()).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MessageAction> for Element {
|
||||
fn from(action: MessageAction) -> Element {
|
||||
let MessageAction { jid, msg, lang } = action;
|
||||
|
||||
Message::chat(Some(jid.into())).with_body(lang, msg).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageAction {
|
||||
/// Generates a new action to send a private message to a parsed [`Jid`]. By default,
|
||||
/// the language used in the message is considered unknown.
|
||||
pub fn new<T: Into<Jid>, U: AsRef<str>>(jid: T, msg: U) -> MessageAction {
|
||||
MessageAction {
|
||||
jid: jid.into(),
|
||||
msg: msg.as_ref().to_string(),
|
||||
lang: "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a new action to send a private message to a stringy JID. By default,
|
||||
/// the language used in the message is considered unknown. Errors if the recipient Jid cannot be
|
||||
/// parsed into a [`Jid`].
|
||||
pub fn new_str<T: AsRef<str>, U: AsRef<str>>(
|
||||
jid: T,
|
||||
msg: U,
|
||||
) -> Result<MessageAction, JidParseError> {
|
||||
Ok(MessageAction {
|
||||
jid: Jid::from_str(jid.as_ref())?,
|
||||
msg: msg.as_ref().to_string(),
|
||||
lang: "".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Specify the language your message is written in, if you know it.
|
||||
pub fn lang(mut self, lang: &str) -> MessageAction {
|
||||
self.lang = lang.to_string();
|
||||
self
|
||||
}
|
||||
}
|
51
xmpp/src/action/mod.rs
Normal file
51
xmpp/src/action/mod.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
//! The actions API allows to create type-safe actions for an [`Agent`]. You need to enable the
|
||||
//! `actions` feature in the `xmpp` crate to use it!
|
||||
//!
|
||||
//! You can build an action manually by generating one of the types exposed in this module,
|
||||
//! or directly call one of the helper methods:
|
||||
//!
|
||||
//! - [`action::message`]
|
||||
//! - [`action::room_join`]
|
||||
//! - [`action::room_message`]
|
||||
//! - [`action::room_private_message`]
|
||||
|
||||
use crate::{Agent, Error, RoomNick};
|
||||
use xmpp_parsers::{BareJid, Jid};
|
||||
|
||||
mod room;
|
||||
pub use room::{RoomJoinAction, RoomMessageAction, RoomPrivateMessageAction};
|
||||
|
||||
mod message;
|
||||
pub use message::MessageAction;
|
||||
|
||||
/// Starts building a [`RoomJoinAction`]
|
||||
pub fn room_join<T: Into<BareJid>, U: AsRef<str>>(room: T, nick: U) -> RoomJoinAction {
|
||||
RoomJoinAction::new(room, nick)
|
||||
}
|
||||
|
||||
/// Starts building a [`RoomMessageAction`]
|
||||
pub fn room_message<T: Into<BareJid>, U: AsRef<str>>(room: T, msg: U) -> RoomMessageAction {
|
||||
RoomMessageAction::new(room, msg)
|
||||
}
|
||||
|
||||
/// Starts building a [`RoomPrivateMessageAction`]
|
||||
pub fn room_private_message<T: Into<BareJid>, U: AsRef<str>>(
|
||||
room: T,
|
||||
nick: RoomNick,
|
||||
msg: U,
|
||||
) -> RoomPrivateMessageAction {
|
||||
RoomPrivateMessageAction::new(room, nick, msg)
|
||||
}
|
||||
|
||||
/// Starts building a [`MessageAction`]
|
||||
pub fn message<T: Into<Jid>, U: AsRef<str>>(room: T, msg: U) -> MessageAction {
|
||||
MessageAction::new(room, msg)
|
||||
}
|
||||
|
||||
/// The trait to implement for typed actions to be consumed by an [`Agent`].
|
||||
/// It contains a single method for the action logic.
|
||||
#[async_trait::async_trait]
|
||||
pub trait Act<T> {
|
||||
/// The method called by the [`Agent`] to perform the action.
|
||||
async fn act(self, client: &mut Agent) -> Result<T, Error>;
|
||||
}
|
128
xmpp/src/action/room/join.rs
Normal file
128
xmpp/src/action/room/join.rs
Normal file
|
@ -0,0 +1,128 @@
|
|||
use crate::{Act, Agent, Error};
|
||||
use std::str::FromStr;
|
||||
use xmpp_parsers::{
|
||||
muc::muc::{History, Muc},
|
||||
presence::Presence,
|
||||
BareJid, Element, JidParseError,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RoomJoinAction {
|
||||
pub room: BareJid,
|
||||
pub nick: String,
|
||||
pub password: Option<String>,
|
||||
pub lang: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub history: Option<History>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Act<()> for RoomJoinAction {
|
||||
async fn act(self, agent: &mut Agent) -> Result<(), Error> {
|
||||
agent.client.send_stanza(self.clone().into()).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomJoinAction> for Element {
|
||||
fn from(action: RoomJoinAction) -> Element {
|
||||
let RoomJoinAction {
|
||||
room,
|
||||
nick,
|
||||
password,
|
||||
lang,
|
||||
status,
|
||||
history,
|
||||
} = action;
|
||||
|
||||
let mut muc = Muc::new();
|
||||
|
||||
if let Some(history) = history {
|
||||
muc = muc.with_history(history);
|
||||
}
|
||||
|
||||
if let Some(password) = password {
|
||||
muc = muc.with_password(password);
|
||||
}
|
||||
|
||||
// TODO: type-safe
|
||||
let room_jid = room.with_resource_str(&nick).unwrap();
|
||||
|
||||
let mut presence = Presence::available()
|
||||
.with_to(room_jid.clone())
|
||||
.with_payload(muc);
|
||||
|
||||
if let Some(status) = status {
|
||||
// lang None is treated as unknown language (empty string)
|
||||
presence.set_status(lang.unwrap_or("".to_string()), status);
|
||||
}
|
||||
|
||||
presence.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl RoomJoinAction {
|
||||
/// Creates an action to join a MUC chatroom from a parsed [`BareJid`]. By default,
|
||||
/// legacy history management is disabled.
|
||||
/// If you really need history from the MUC, you can use [`JoinRoomAction::history_default`], but be careful
|
||||
/// because there will be no way to distinguish them from normal live messages, see also:
|
||||
/// https://gitlab.com/xmpp-rs/xmpp-rs/-/issues/77
|
||||
pub fn new<T: Into<BareJid>, U: AsRef<str>>(room: T, nick: U) -> RoomJoinAction {
|
||||
RoomJoinAction {
|
||||
room: room.into(),
|
||||
nick: nick.as_ref().to_string(),
|
||||
password: None,
|
||||
lang: None,
|
||||
status: None,
|
||||
history: Some(History::new().with_maxchars(0)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an action to join a MUC chatroom from a stringy bare JID. By default,
|
||||
/// legacy history management is disabled. Errors if the room JID is not parseable into a [`BareJid`].
|
||||
/// If you really need history from the MUC, you can use [`JoinRoomAction::history_default`], but be careful
|
||||
/// because there will be no way to distinguish them from normal live messages, see also:
|
||||
/// https://gitlab.com/xmpp-rs/xmpp-rs/-/issues/77
|
||||
pub fn new_str<T: AsRef<str>, U: AsRef<str>>(
|
||||
room: T,
|
||||
nick: U,
|
||||
) -> Result<RoomJoinAction, JidParseError> {
|
||||
Ok(RoomJoinAction {
|
||||
room: BareJid::from_str(room.as_ref())?,
|
||||
nick: nick.as_ref().to_string(),
|
||||
password: None,
|
||||
lang: None,
|
||||
status: None,
|
||||
history: Some(History::new().with_maxchars(0)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets the nick associated with the client on the chatroom to be joined.
|
||||
pub fn nick(mut self, nick: &str) -> RoomJoinAction {
|
||||
self.nick = nick.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the password required to enter the MUC chatroom.
|
||||
pub fn password(mut self, password: &str) -> RoomJoinAction {
|
||||
self.password = Some(password.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets a status associated with the client, when joining the room. The status
|
||||
/// should define a language associated with it, but None will be treated as explicitly
|
||||
/// not knowing the language, i.e. `xml:lang=""` as explained [here](https://gitlab.com/xmpp-rs/xmpp-rs/-/issues/80).
|
||||
pub fn status(mut self, lang: Option<String>, status: &str) -> RoomJoinAction {
|
||||
if let Some(lang) = lang {
|
||||
self.lang = Some(lang);
|
||||
}
|
||||
self.status = Some(status.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Request the default history (as set by the server) from legacy MUC history.
|
||||
pub fn history_default(mut self) -> RoomJoinAction {
|
||||
self.history = None;
|
||||
self
|
||||
}
|
||||
}
|
59
xmpp/src/action/room/message.rs
Normal file
59
xmpp/src/action/room/message.rs
Normal file
|
@ -0,0 +1,59 @@
|
|||
use crate::{Act, Agent, Error};
|
||||
use std::str::FromStr;
|
||||
use xmpp_parsers::{message::Message, BareJid, Element, JidParseError};
|
||||
|
||||
pub struct RoomMessageAction {
|
||||
pub room: BareJid,
|
||||
pub msg: String,
|
||||
pub lang: String,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Act<()> for RoomMessageAction {
|
||||
async fn act(self, agent: &mut Agent) -> Result<(), Error> {
|
||||
agent.client.send_stanza(self.into()).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomMessageAction> for Element {
|
||||
fn from(action: RoomMessageAction) -> Element {
|
||||
let RoomMessageAction { room, msg, lang } = action;
|
||||
|
||||
Message::groupchat(Some(room.into()))
|
||||
.with_body(lang, msg)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl RoomMessageAction {
|
||||
/// Generates a new action to send a group message to a parsed MUC chatroom [`BareJid`]. By default,
|
||||
/// the language used in the message is considered unknown.
|
||||
pub fn new<T: Into<BareJid>, U: AsRef<str>>(room: T, msg: U) -> RoomMessageAction {
|
||||
RoomMessageAction {
|
||||
room: room.into(),
|
||||
msg: msg.as_ref().to_string(),
|
||||
lang: "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a new action to send a group message to a stringy MUC chatroom. By default,
|
||||
/// the language used in the message is considered unknown. Errors if the chatroom Jid cannot be
|
||||
/// parsed into a [`BareJid`].
|
||||
pub fn new_str<T: AsRef<str>, U: AsRef<str>>(
|
||||
room: T,
|
||||
msg: U,
|
||||
) -> Result<RoomMessageAction, JidParseError> {
|
||||
Ok(RoomMessageAction {
|
||||
room: BareJid::from_str(room.as_ref())?,
|
||||
msg: msg.as_ref().to_string(),
|
||||
lang: "".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Specify the language your message is written in, if you know it.
|
||||
pub fn lang(mut self, lang: &str) -> RoomMessageAction {
|
||||
self.lang = lang.to_string();
|
||||
self
|
||||
}
|
||||
}
|
8
xmpp/src/action/room/mod.rs
Normal file
8
xmpp/src/action/room/mod.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
mod join;
|
||||
pub use join::RoomJoinAction;
|
||||
|
||||
mod message;
|
||||
pub use message::RoomMessageAction;
|
||||
|
||||
mod private_message;
|
||||
pub use private_message::RoomPrivateMessageAction;
|
78
xmpp/src/action/room/private_message.rs
Normal file
78
xmpp/src/action/room/private_message.rs
Normal file
|
@ -0,0 +1,78 @@
|
|||
use crate::{Act, Agent, Error, RoomNick};
|
||||
use std::str::FromStr;
|
||||
use xmpp_parsers::{message::Message, muc::MucUser, BareJid, Element, JidParseError};
|
||||
|
||||
pub struct RoomPrivateMessageAction {
|
||||
pub room: BareJid,
|
||||
pub recipient: RoomNick,
|
||||
pub msg: String,
|
||||
pub lang: String,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Act<()> for RoomPrivateMessageAction {
|
||||
async fn act(self, agent: &mut Agent) -> Result<(), Error> {
|
||||
agent.client.send_stanza(self.into()).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomPrivateMessageAction> for Element {
|
||||
fn from(action: RoomPrivateMessageAction) -> Element {
|
||||
let RoomPrivateMessageAction {
|
||||
room,
|
||||
recipient,
|
||||
msg,
|
||||
lang,
|
||||
} = action;
|
||||
|
||||
// TODO: type-safe
|
||||
let recipient = room.with_resource_str(&recipient).unwrap();
|
||||
|
||||
Message::chat(Some(recipient.into()))
|
||||
.with_body(lang, msg)
|
||||
.with_payload(MucUser::new())
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl RoomPrivateMessageAction {
|
||||
/// Generates a new action to send a private a parsed MUC participant
|
||||
/// (room [`BareJid`] + participant [`RoomNick`].
|
||||
/// By default, the language used in the message is considered unknown.
|
||||
pub fn new<T: Into<BareJid>, U: AsRef<str>>(
|
||||
room: T,
|
||||
recipient: RoomNick,
|
||||
msg: U,
|
||||
) -> RoomPrivateMessageAction {
|
||||
RoomPrivateMessageAction {
|
||||
room: room.into(),
|
||||
recipient,
|
||||
msg: msg.as_ref().to_string(),
|
||||
lang: "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a new action to send a private message to a stringy MUC participant
|
||||
/// (stringy room + stringy participant).
|
||||
/// By default, the language used in the message is considered unknown.
|
||||
/// Errors if the chatroom Jid cannot be parsed into a [`BareJid`].
|
||||
pub fn new_str<T: AsRef<str>, U: AsRef<str>>(
|
||||
room: T,
|
||||
recipient: RoomNick,
|
||||
msg: U,
|
||||
) -> Result<RoomPrivateMessageAction, JidParseError> {
|
||||
Ok(RoomPrivateMessageAction {
|
||||
room: BareJid::from_str(room.as_ref())?,
|
||||
recipient,
|
||||
msg: msg.as_ref().to_string(),
|
||||
lang: "".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Specify the language your message is written in, if you know it.
|
||||
pub fn lang(mut self, lang: &str) -> RoomPrivateMessageAction {
|
||||
self.lang = lang.to_string();
|
||||
self
|
||||
}
|
||||
}
|
|
@ -40,6 +40,11 @@ extern crate log;
|
|||
|
||||
mod pubsub;
|
||||
|
||||
#[cfg(feature = "actions")]
|
||||
pub mod action;
|
||||
#[cfg(feature = "actions")]
|
||||
pub use action::Act;
|
||||
|
||||
pub type Error = tokio_xmpp::Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -219,6 +224,11 @@ impl Agent {
|
|||
self.client.send_end().await
|
||||
}
|
||||
|
||||
#[cfg(feature = "actions")]
|
||||
pub async fn action<Success, T: Act<Success>>(&mut self, action: T) -> Result<Success, Error> {
|
||||
action.act(self).await
|
||||
}
|
||||
|
||||
pub async fn join_room(
|
||||
&mut self,
|
||||
room: BareJid,
|
||||
|
|
Loading…
Reference in a new issue