Compare commits

...

8 Commits

Author SHA1 Message Date
5b36300054 sendxmpp-rs v3.0.1
Some checks failed
moparisthebest/sendxmpp-rs/pipeline/head There was a failure building this commit
2022-11-04 23:18:50 -04:00
7aff2080df Update tokio-xmpp, switch to rustls
Some checks failed
moparisthebest/sendxmpp-rs/pipeline/head There was a failure building this commit
2022-11-04 22:59:46 -04:00
294da89d22 sendxmpp-rs v3.0.0
Some checks failed
moparisthebest/sendxmpp-rs/pipeline/head There was a failure building this commit
2022-11-04 21:52:14 -04:00
f1aae8e3c2
Merge pull request #5 from Ppjet6/muc-support
Muc support
2022-11-04 21:26:18 -04:00
c8ce4bb629
Merge pull request #4 from pteromys/master
Add env_logger to ease troubleshooting.
2022-11-04 21:24:14 -04:00
Maxime “pep” Buquet
3b36852749
Allow optional nick in config file
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-09-25 11:13:43 +02:00
Maxime “pep” Buquet
06ecb97f4c Initial MUC support
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-09-25 00:42:38 +02:00
pteromys
b643665354 Add env_logger to ease troubleshooting.
Apparently I had an empty /etc/resolv.conf on one host due to systemd
growing pains, which I only discovered by adding the logger and seeing

> [2021-12-31T20:22:01Z WARN  trust_dns_resolver::system_conf::unix] no nameservers found in config

Without env_logger, the only output I had was an unhelpful

> could not connect to xmpp server

and changing die() to unwrap() only narrowed it down slightly to

> Connection(Resolve(ResolveError { kind: Message("No connections available") }))

One can set the environment variable RUST_LOG=debug for more verbosity.
Under the default setting it seems to be silent if nothing goes wrong.
2021-12-31 16:01:04 -05:00
5 changed files with 671 additions and 805 deletions

1356
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package]
name = "sendxmpp"
version = "2.0.0"
version = "3.0.1"
authors = ["moparisthebest <admin@moparisthebest.com>"]
description = "Send XMPP messages from the command line."
@ -25,11 +25,10 @@ serde_derive = "1.0"
serde = { version = "1.0", features = ["derive"] }
gumdrop = "0.8.0"
gumdrop_derive = "0.8.0"
dirs = "3.0.1"
tokio-xmpp = "3.0.0"
dirs = "4.0.0"
tokio-xmpp = { version = "3.2.0", default-features = false, features = ["tls-rust"] }
tokio = { version = "1", features = ["net", "rt", "rt-multi-thread", "macros", "io-util", "io-std"] }
tokio-tls = { package = "tokio-native-tls", version = "0.3" }
xmpp-parsers = "0.18"
xmpp-parsers = "0.19"
die = "0.2.0"
anyhow = "1.0"
env_logger = "0.9"

View File

@ -18,6 +18,10 @@ Optional arguments:
-c, --config CONFIG path to config file. default: ~/.config/sendxmpp.toml with fallback to /etc/sendxmpp/sendxmpp.toml
-e, --force-pgp Force OpenPGP encryption for all recipients
-a, --attempt-pgp Attempt OpenPGP encryption for all recipients
-r, --raw Send raw XML stream, cannot be used with recipients or PGP
-p, --presence Send a <presence/> after connecting before sending messages, required for receiving for --raw
-m, --muc Recipients are Multi-User Chats
-n, --nick NICK Nickname to use in Multi-User Chats
```
Usage examples:

View File

@ -1,4 +1,5 @@
# jid and password exactly like this, nothing else
# jid and password exactly like this
jid = "jid@example.org"
password = "sOmePa55W0rD"
# nick = "foobar" # optional nick for Multi-User Chat

View File

@ -3,19 +3,20 @@ use std::fs::File;
use std::io::{stdin, Read, Write};
use std::iter::Iterator;
use std::path::Path;
use std::str::FromStr;
use die::{die, Die};
use gumdrop::Options;
use serde_derive::Deserialize;
use std::process::{Command, Stdio};
use tokio_xmpp::{xmpp_stream, SimpleClient as Client};
use xmpp_parsers::message::{Body, Message};
use tokio_xmpp::{SimpleClient as Client};
use xmpp_parsers::message::{Body, Message, MessageType};
use xmpp_parsers::muc::Muc;
use xmpp_parsers::presence::{Presence, Show as PresenceShow, Type as PresenceType};
use xmpp_parsers::{Element, Jid};
use xmpp_parsers::{BareJid, Element, FullJid, Jid};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio_tls::TlsStream;
use anyhow::{anyhow, bail, Result};
@ -23,6 +24,7 @@ use anyhow::{anyhow, bail, Result};
struct Config {
jid: String,
password: String,
nick: Option<String>,
}
fn parse_cfg<P: AsRef<Path>>(path: P) -> Result<Config> {
@ -54,10 +56,18 @@ struct MyOptions {
#[options(help = "Send a <presence/> after connecting before sending messages, required for receiving for --raw")]
presence: bool,
#[options(help = "Recipients are Multi-User Chats")]
muc: bool,
#[options(help = "Nickname to use in Multi-User Chats")]
nick: Option<String>,
}
#[tokio::main]
async fn main() {
env_logger::init();
let args: Vec<String> = args().collect();
// Remember to skip the first argument. That's the program name.
@ -79,10 +89,19 @@ async fn main() {
if !recipients.is_empty() {
die!("--raw is incompatible with recipients");
}
if opts.muc {
die!("--raw is incompatible with --muc");
}
} else if recipients.is_empty() {
die!("no recipients specified!");
}
if opts.muc {
if opts.force_pgp || opts.attempt_pgp {
die!("--force-pgp and --attempt-pgp isn't implemented with --muc");
}
}
let recipients = &recipients;
let cfg = match opts.config {
@ -101,19 +120,13 @@ async fn main() {
// can paste this to test: <message xmlns="jabber:client" to="travis@burtrum.org" type="chat"><body>woot</body></message>
pub struct OpenClient {
pub stream: xmpp_stream::XMPPStream<TlsStream<tokio::net::TcpStream>>,
}
let client: OpenClient = unsafe { std::mem::transmute(client) };
let mut open_client = client.stream.stream.try_lock().die("could not lock client stream");
let open_client = open_client.get_mut();
let mut open_client = client.into_inner().into_inner();
let mut rd_buf = [0u8; 256]; // todo: proper buffer size?
let mut stdin_buf = rd_buf.clone();
let mut stdin = tokio::io::stdin();
let mut stdout = tokio::io::stdout();
loop {
tokio::select! {
n = open_client.read(&mut rd_buf) => {
@ -143,6 +156,10 @@ async fn main() {
let mut data = String::new();
stdin().lock().read_to_string(&mut data).die("error reading from stdin");
let data = data.trim();
if data.is_empty() {
// don't send empty stanzas
return;
}
let mut client = Client::new(&cfg.jid, &cfg.password).await.die("could not connect to xmpp server");
@ -151,27 +168,45 @@ async fn main() {
}
for recipient in recipients {
let reply = if opts.force_pgp || opts.attempt_pgp {
let encrypted = gpg_encrypt(recipient.clone(), &data);
if encrypted.is_err() {
if opts.force_pgp {
die!("pgp encryption to jid '{}' failed!", recipient);
if opts.muc {
let nick = opts
.nick
.clone()
.or(cfg.nick.clone())
.or_else(|| BareJid::from_str(cfg.jid.as_str()).unwrap().node)
.die("couldn't find a nick to use");
let participant = match recipient.clone() {
Jid::Full(_) => die!("Invalid room address"),
Jid::Bare(bare) => bare.with_resource(nick.clone()),
};
let join = make_join(participant.clone());
client.send_stanza(join).await.die("failed to join MUC");
let reply = make_reply(recipient.clone(), &data, opts.muc);
client.send_stanza(reply).await.die("sending message failed");
} else {
let reply = if opts.force_pgp || opts.attempt_pgp {
let encrypted = gpg_encrypt(recipient.clone(), &data);
if encrypted.is_err() {
if opts.force_pgp {
die!("pgp encryption to jid '{}' failed!", recipient);
} else {
make_reply(recipient.clone(), &data, opts.muc)
}
} else {
make_reply(recipient.clone(), &data)
let encrypted = encrypted.unwrap();
let encrypted = encrypted.trim();
let mut reply = make_reply(recipient.clone(), "pgp", opts.muc);
let mut x = Element::bare("x", "jabber:x:encrypted");
x.append_text_node(encrypted);
reply.append_child(x);
reply
}
} else {
let encrypted = encrypted.unwrap();
let encrypted = encrypted.trim();
let mut reply = make_reply(recipient.clone(), "pgp");
let mut x = Element::bare("x", "jabber:x:encrypted");
x.append_text_node(encrypted);
reply.append_child(x);
reply
}
} else {
make_reply(recipient.clone(), &data)
};
client.send_stanza(reply).await.die("sending message failed");
make_reply(recipient.clone(), &data, opts.muc)
};
client.send_stanza(reply).await.die("sending message failed");
}
}
// Close client connection
@ -186,9 +221,16 @@ fn make_presence() -> Element {
presence.into()
}
fn make_join(to: FullJid) -> Element {
Presence::new(PresenceType::None).with_to(Jid::Full(to)).with_payloads(vec![Muc::new().into()]).into()
}
// Construct a chat <message/>
fn make_reply(to: Jid, body: &str) -> Element {
fn make_reply(to: Jid, body: &str, groupchat: bool) -> Element {
let mut message = Message::new(Some(to));
if groupchat {
message.type_ = MessageType::Groupchat;
}
message.bodies.insert(String::new(), Body(body.to_owned()));
message.into()
}