xmpp: HTTP File Upload
Implement XEP-0363 HTTP File Upload in xmpp-rs. The current interface is as is because of a limitation of our libraries. It is not possible to await on an IQ to get a result, so we have to workaround it by storing data on the Agent and fetching it back when we get the result. The client will have to first call `upload_file_with` and then listen on Event::HttpUploadedFile, which are decoupled actions, instead of awaiting on upload_file_with and getting the URL as a result directly. `upload_file_with` doesn't yet find the upload service by itself for the same reason as above. Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
This commit is contained in:
parent
eb19f8329c
commit
a59c618ed0
2 changed files with 66 additions and 2 deletions
|
|
@ -17,8 +17,10 @@ edition = "2018"
|
||||||
tokio-xmpp = "3.0.0"
|
tokio-xmpp = "3.0.0"
|
||||||
xmpp-parsers = "0.19"
|
xmpp-parsers = "0.19"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
tokio = "1"
|
tokio = { version = "1", features = ["full"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
reqwest = { version = "0.11.8", features = ["stream"] }
|
||||||
|
tokio-util = { version = "0.6.9", features = ["codec"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
env_logger = "0.8"
|
env_logger = "0.8"
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,20 @@
|
||||||
#![deny(bare_trait_objects)]
|
#![deny(bare_trait_objects)]
|
||||||
|
|
||||||
use futures::stream::StreamExt;
|
use futures::stream::StreamExt;
|
||||||
|
use reqwest::{Body as ReqwestBody, Client as ReqwestClient};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use tokio::fs::File;
|
||||||
|
use tokio_util::codec::{BytesCodec, FramedRead};
|
||||||
use tokio_xmpp::{AsyncClient as TokioXmppClient, Event as TokioXmppEvent};
|
use tokio_xmpp::{AsyncClient as TokioXmppClient, Event as TokioXmppEvent};
|
||||||
use xmpp_parsers::{
|
use xmpp_parsers::{
|
||||||
bookmarks2::Conference,
|
bookmarks2::Conference,
|
||||||
caps::{compute_disco, hash_caps, Caps},
|
caps::{compute_disco, hash_caps, Caps},
|
||||||
disco::{DiscoInfoQuery, DiscoInfoResult, Feature, Identity},
|
disco::{DiscoInfoQuery, DiscoInfoResult, Feature, Identity},
|
||||||
hashes::Algo,
|
hashes::Algo,
|
||||||
|
http_upload::{SlotRequest, SlotResult},
|
||||||
iq::{Iq, IqType},
|
iq::{Iq, IqType},
|
||||||
message::{Body, Message, MessageType},
|
message::{Body, Message, MessageType},
|
||||||
muc::{
|
muc::{
|
||||||
|
|
@ -27,7 +32,7 @@ use xmpp_parsers::{
|
||||||
pubsub::pubsub::{Items, PubSub},
|
pubsub::pubsub::{Items, PubSub},
|
||||||
roster::{Item as RosterItem, Roster},
|
roster::{Item as RosterItem, Roster},
|
||||||
stanza_error::{DefinedCondition, ErrorType, StanzaError},
|
stanza_error::{DefinedCondition, ErrorType, StanzaError},
|
||||||
BareJid, FullJid, Jid,
|
BareJid, Element, FullJid, Jid,
|
||||||
};
|
};
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
|
|
@ -83,6 +88,7 @@ pub enum Event {
|
||||||
RoomJoined(BareJid),
|
RoomJoined(BareJid),
|
||||||
RoomLeft(BareJid),
|
RoomLeft(BareJid),
|
||||||
RoomMessage(BareJid, RoomNick, Body),
|
RoomMessage(BareJid, RoomNick, Body),
|
||||||
|
HttpUploadedFile(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|
@ -175,6 +181,7 @@ impl ClientBuilder<'_> {
|
||||||
lang: Rc::new(self.lang),
|
lang: Rc::new(self.lang),
|
||||||
disco,
|
disco,
|
||||||
node,
|
node,
|
||||||
|
uploads: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(agent)
|
Ok(agent)
|
||||||
|
|
@ -187,6 +194,7 @@ pub struct Agent {
|
||||||
lang: Rc<Vec<String>>,
|
lang: Rc<Vec<String>>,
|
||||||
disco: DiscoInfoResult,
|
disco: DiscoInfoResult,
|
||||||
node: String,
|
node: String,
|
||||||
|
uploads: Vec<(String, Jid, PathBuf)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Agent {
|
impl Agent {
|
||||||
|
|
@ -291,6 +299,9 @@ impl Agent {
|
||||||
} else if payload.is("pubsub", ns::PUBSUB) {
|
} else if payload.is("pubsub", ns::PUBSUB) {
|
||||||
let new_events = pubsub::handle_iq_result(&from, payload);
|
let new_events = pubsub::handle_iq_result(&from, payload);
|
||||||
events.extend(new_events);
|
events.extend(new_events);
|
||||||
|
} else if payload.is("slot", ns::HTTP_UPLOAD) {
|
||||||
|
let new_events = handle_upload_result(&from, iq.id, payload, self).await;
|
||||||
|
events.extend(new_events);
|
||||||
}
|
}
|
||||||
} else if let IqType::Set(_) = iq.payload {
|
} else if let IqType::Set(_) = iq.payload {
|
||||||
// We MUST answer unhandled set iqs with a service-unavailable error.
|
// We MUST answer unhandled set iqs with a service-unavailable error.
|
||||||
|
|
@ -417,6 +428,57 @@ impl Agent {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn upload_file_with(&mut self, service: &str, path: &Path) {
|
||||||
|
let name = path.file_name().unwrap().to_str().unwrap().to_string();
|
||||||
|
let file = File::open(path).await.unwrap();
|
||||||
|
let size = file.metadata().await.unwrap().len();
|
||||||
|
let slot_request = SlotRequest {
|
||||||
|
filename: name,
|
||||||
|
size: size,
|
||||||
|
content_type: None,
|
||||||
|
};
|
||||||
|
let to = service.parse::<Jid>().unwrap();
|
||||||
|
let request = Iq::from_get("upload1", slot_request).with_to(to.clone());
|
||||||
|
self.uploads
|
||||||
|
.push((String::from("upload1"), to, path.to_path_buf()));
|
||||||
|
self.client.send_stanza(request.into()).await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_upload_result(
|
||||||
|
from: &Jid,
|
||||||
|
iqid: String,
|
||||||
|
elem: Element,
|
||||||
|
agent: &mut Agent,
|
||||||
|
) -> impl IntoIterator<Item = Event> {
|
||||||
|
let mut res: Option<(usize, PathBuf)> = None;
|
||||||
|
|
||||||
|
for (i, (id, to, filepath)) in agent.uploads.iter().enumerate() {
|
||||||
|
if to == from && id == &iqid {
|
||||||
|
res = Some((i, filepath.to_path_buf()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((index, file)) = res {
|
||||||
|
agent.uploads.remove(index);
|
||||||
|
let slot = SlotResult::try_from(elem).unwrap();
|
||||||
|
let web = ReqwestClient::new();
|
||||||
|
let stream = FramedRead::new(File::open(file).await.unwrap(), BytesCodec::new());
|
||||||
|
let body = ReqwestBody::wrap_stream(stream);
|
||||||
|
let res = web
|
||||||
|
.put(slot.put.url.as_str())
|
||||||
|
.body(body)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
if res.status() == 201 {
|
||||||
|
return vec![Event::HttpUploadedFile(slot.get.url)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return vec![];
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue