WIP: Actions API

This commit is contained in:
xmppftw 2023-06-06 18:20:22 +02:00
parent f9d4419513
commit 92bad0038d
9 changed files with 603 additions and 1 deletions

View file

@ -154,6 +154,14 @@ impl Jid {
Jid::Bare(BareJid { domain, .. }) | Jid::Full(FullJid { domain, .. }) => domain,
}
}
/// The resource part of the Jabber ID.
pub fn resource(self) -> Option<String> {
match self {
Jid::Bare(_) => None,
Jid::Full(full_jid) => Some(full_jid.resource),
}
}
}
impl From<Jid> for BareJid {

View file

@ -17,14 +17,22 @@ edition = "2018"
tokio-xmpp = "3.2"
xmpp-parsers = "0.19"
futures = "0.3"
tokio = { version = "1", features = ["fs"] }
tokio = { version = "1", features = ["fs", "sync" ] }
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" ]

View file

@ -0,0 +1,152 @@
// Copyright (c) 2019 Emmanuel Gil Peyrot <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 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::join_room(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::group_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::group_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::private_message(
jid,
format!("You said: {}", body.0),
))
.await?;
}
Event::RoomJoined(jid) => {
println!("Joined room {}.", jid);
client
.action(Action::group_message(jid, "Hello, world!"))
.await?;
}
_ => (),
}
}
}
Ok(())
}

View file

@ -0,0 +1,65 @@
use crate::{Act, Action, Agent, Error};
use std::str::FromStr;
use xmpp_parsers::{message::Message, BareJid, Element, JidParseError};
pub struct GroupMessageAction {
pub room: BareJid,
pub msg: String,
pub lang: String,
}
#[async_trait]
impl Act<()> for GroupMessageAction {
async fn act(self, agent: &mut Agent) -> Result<(), Error> {
agent.client.send_stanza(self.into()).await?;
Ok(())
}
}
impl From<GroupMessageAction> for Action {
fn from(action: GroupMessageAction) -> Action {
Action::GroupMessage(action)
}
}
impl From<GroupMessageAction> for Element {
fn from(action: GroupMessageAction) -> Element {
let GroupMessageAction { room, msg, lang } = action;
Message::groupchat(Some(room.into()))
.with_body(lang, msg)
.into()
}
}
impl GroupMessageAction {
/// 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) -> GroupMessageAction {
GroupMessageAction {
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<GroupMessageAction, JidParseError> {
Ok(GroupMessageAction {
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) -> GroupMessageAction {
self.lang = lang.to_string();
self
}
}

View file

@ -0,0 +1,80 @@
use crate::{Act, Action, Agent, Error, RoomNick};
use std::str::FromStr;
use xmpp_parsers::{message::Message, muc::MucUser, BareJid, Element, JidParseError};
pub struct GroupPrivateMessageAction {
pub room: BareJid,
pub recipient: RoomNick,
pub msg: String,
pub lang: String,
}
#[async_trait]
impl Act<()> for GroupPrivateMessageAction {
async fn act(self, agent: &mut Agent) -> Result<(), Error> {
agent.client.send_stanza(self.into()).await?;
Ok(())
}
}
impl From<GroupPrivateMessageAction> for Action {
fn from(action: GroupPrivateMessageAction) -> Action {
Action::GroupPrivateMessage(action)
}
}
impl From<GroupPrivateMessageAction> for Element {
fn from(action: GroupPrivateMessageAction) -> Element {
let GroupPrivateMessageAction {
room,
recipient,
msg,
lang,
} = action;
let recipient = room.with_resource(recipient);
Message::chat(Some(recipient.into()))
.with_body(lang, msg)
.with_payload(MucUser::new())
.into()
}
}
impl GroupPrivateMessageAction {
/// 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,
) -> GroupPrivateMessageAction {
GroupPrivateMessageAction {
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<GroupPrivateMessageAction, JidParseError> {
Ok(GroupPrivateMessageAction {
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) -> GroupPrivateMessageAction {
self.lang = lang.to_string();
self
}
}

View file

@ -0,0 +1,151 @@
use crate::{Act, Action, Agent, Error};
use std::str::FromStr;
use xmpp_parsers::{
date::DateTime,
muc::muc::{History, Muc},
presence::Presence,
BareJid, Element, JidParseError,
};
#[derive(Clone, Debug)]
pub struct JoinRoomAction {
pub room: BareJid,
pub nick: String,
pub password: Option<String>,
pub lang: Option<String>,
pub status: Option<String>,
pub history: History,
}
#[async_trait]
impl Act<()> for JoinRoomAction {
async fn act(self, agent: &mut Agent) -> Result<(), Error> {
agent.client.send_stanza(self.clone().into()).await?;
Ok(())
}
}
impl From<JoinRoomAction> for Action {
fn from(action: JoinRoomAction) -> Action {
Action::JoinRoom(action)
}
}
impl From<JoinRoomAction> for Element {
fn from(action: JoinRoomAction) -> Element {
let JoinRoomAction {
room,
nick,
password,
lang,
status,
history,
} = action;
let mut muc = Muc::new().with_history(history);
if let Some(password) = password {
muc = muc.with_password(password);
}
let room_jid = room.with_resource(nick);
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 JoinRoomAction {
/// Creates an [`Action`] to join a MUC chatroom from a parsed [`BareJid`]. By default,
/// legacy history management is disabled. In the future, it should be replaced with MAM, but if
/// you really need legacy history right now we still provide helpers for it.
pub fn new<T: Into<BareJid>, U: AsRef<str>>(room: T, nick: U) -> JoinRoomAction {
JoinRoomAction {
room: room.into(),
nick: nick.as_ref().to_string(),
password: None,
lang: None,
status: None,
history: 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`].
/// In the future, it should be replaced with MAM, but if you really need legacy history right now we still
/// provide helpers for it.
pub fn new_str<T: AsRef<str>, U: AsRef<str>>(
room: T,
nick: U,
) -> Result<JoinRoomAction, JidParseError> {
Ok(JoinRoomAction {
room: BareJid::from_str(room.as_ref())?,
nick: nick.as_ref().to_string(),
password: None,
lang: None,
status: None,
history: 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) -> JoinRoomAction {
self.nick = nick.to_string();
self
}
/// Sets the password required to enter the MUC chatroom.
pub fn password(mut self, password: &str) -> JoinRoomAction {
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) -> JoinRoomAction {
if let Some(lang) = lang {
self.lang = Some(lang);
}
self.status = Some(status.to_string());
self
}
/// Request specific [`History`] settings from legacy MUC history.
pub fn history(mut self, history: History) -> JoinRoomAction {
self.history = history;
self
}
/// Request a maximum of `maxchars` characters from legacy MUC history.
pub fn history_maxchars(mut self, maxchars: u32) -> JoinRoomAction {
self.history = History::new().with_maxchars(maxchars);
self
}
/// Request a maximum of `maxstanzas` stanzas from legacy MUC history.
pub fn history_maxstanzas(mut self, maxstanzas: u32) -> JoinRoomAction {
self.history = History::new().with_maxstanzas(maxstanzas);
self
}
/// Request all messages since `sec` seconds from legacy MUC history.
pub fn history_since_sec(mut self, sec: u32) -> JoinRoomAction {
self.history = History::new().with_seconds(sec);
self
}
/// Request all messages since `since` [`DateTime`] from legacy MUC history.
pub fn history_since(mut self, since: DateTime) -> JoinRoomAction {
self.history = History::new().with_since(since);
self
}
}

62
xmpp/src/action/mod.rs Normal file
View file

@ -0,0 +1,62 @@
//! The [`Action`] API allows to create type-safe actions for an [`Agent`].
//!
//! You can build an Action manually by generating one of the enum variants, or use one of the associated builders:
//!
//! - [`Action::join_room`]
//! - [`Action::private_message`]
//! - [`Action::group_message`]
//! - [`Action::group_private_message`]
use crate::{Agent, Error, RoomNick};
use xmpp_parsers::{BareJid, Jid};
pub mod join_room;
pub use join_room::JoinRoomAction;
pub mod group_message;
pub use group_message::GroupMessageAction;
pub mod group_private_message;
pub use group_private_message::GroupPrivateMessageAction;
pub mod private_message;
pub use private_message::PrivateMessageAction;
pub enum Action {
///
JoinRoom(JoinRoomAction),
GroupMessage(GroupMessageAction),
GroupPrivateMessage(GroupPrivateMessageAction),
PrivateMessage(PrivateMessageAction),
}
impl Action {
/// Starts building a [`JoinRoomAction`]
pub fn join_room<T: Into<BareJid>, U: AsRef<str>>(room: T, nick: U) -> JoinRoomAction {
JoinRoomAction::new(room, nick)
}
/// Starts building a [`GroupMessageAction`]
pub fn group_message<T: Into<BareJid>, U: AsRef<str>>(room: T, msg: U) -> GroupMessageAction {
GroupMessageAction::new(room, msg)
}
/// Starts building a [`GroupPrivateMessageAction`]
pub fn group_private_message<T: Into<BareJid>, U: AsRef<str>>(
room: T,
nick: RoomNick,
msg: U,
) -> GroupPrivateMessageAction {
GroupPrivateMessageAction::new(room, nick, msg)
}
/// Starts building a [`PrivateMessageAction`]
pub fn private_message<T: Into<Jid>, U: AsRef<str>>(room: T, msg: U) -> PrivateMessageAction {
PrivateMessageAction::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]
pub trait Act<T> {
/// The method called by the [`Agent`] to perform the action.
async fn act(self, client: &mut Agent) -> Result<T, Error>;
}

View file

@ -0,0 +1,63 @@
use crate::{Act, Action, Agent, Error};
use std::str::FromStr;
use xmpp_parsers::{message::Message, Element, Jid, JidParseError};
pub struct PrivateMessageAction {
pub jid: Jid,
pub msg: String,
pub lang: String,
}
#[async_trait]
impl Act<()> for PrivateMessageAction {
async fn act(self, agent: &mut Agent) -> Result<(), Error> {
agent.client.send_stanza(self.into()).await?;
Ok(())
}
}
impl From<PrivateMessageAction> for Action {
fn from(action: PrivateMessageAction) -> Action {
Action::PrivateMessage(action)
}
}
impl From<PrivateMessageAction> for Element {
fn from(action: PrivateMessageAction) -> Element {
let PrivateMessageAction { jid, msg, lang } = action;
Message::chat(Some(jid.into())).with_body(lang, msg).into()
}
}
impl PrivateMessageAction {
/// 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) -> PrivateMessageAction {
PrivateMessageAction {
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<PrivateMessageAction, JidParseError> {
Ok(PrivateMessageAction {
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) -> PrivateMessageAction {
self.lang = lang.to_string();
self
}
}

View file

@ -40,6 +40,15 @@ extern crate log;
mod pubsub;
#[cfg(feature = "actions")]
#[macro_use]
extern crate async_trait;
#[cfg(feature = "actions")]
pub mod action;
#[cfg(feature = "actions")]
pub use action::{Act, Action};
pub type Error = tokio_xmpp::Error;
#[derive(Debug)]
@ -219,6 +228,10 @@ impl Agent {
self.client.send_end().await
}
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,