mod bounding_box;
pub use http::Method as RequestMethod;
pub use http::Uri;
pub use bounding_box::BoundingBox;
use std::borrow::Cow;
use std::fmt::{self, Formatter};
use http::header::{HeaderValue, AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE};
use http::Request;
use slice_of_array::SliceFlatExt;
use crate::service::HttpService;
use crate::util::fmt_join;
use crate::{FutureTwitterStream, Token};
#[derive(Clone, Debug)]
pub struct Builder<'a, T = Token> {
token: T,
endpoint: Option<(RequestMethod, Uri)>,
parameters: Parameters<'a>,
}
#[derive(Clone, Debug, Default, oauth::Request)]
struct Parameters<'a> {
#[oauth1(skip_if = not)]
stall_warnings: bool,
filter_level: Option<FilterLevel>,
#[oauth1(skip_if = str::is_empty)]
language: Cow<'a, str>,
#[oauth1(encoded, fmt = fmt_follow, skip_if = <[_]>::is_empty)]
follow: Cow<'a, [u64]>,
#[oauth1(skip_if = str::is_empty)]
track: Cow<'a, str>,
#[oauth1(encoded, fmt = fmt_locations, skip_if = <[_]>::is_empty)]
#[allow(clippy::type_complexity)]
locations: Cow<'a, [BoundingBox]>,
#[oauth1(encoded)]
count: Option<i32>,
}
str_enum! {
#[derive(Clone, Debug, PartialEq, Hash, Eq)]
pub enum FilterLevel {
None = "none",
Low = "low",
Medium = "medium",
}
}
const FILTER: &str = "https://stream.twitter.com/1.1/statuses/filter.json";
const SAMPLE: &str = "https://stream.twitter.com/1.1/statuses/sample.json";
impl<'a, C, A> Builder<'a, Token<C, A>>
where
C: AsRef<str>,
A: AsRef<str>,
{
pub fn new(token: Token<C, A>) -> Self {
Builder {
token,
endpoint: None,
parameters: Parameters::default(),
}
}
#[cfg(feature = "hyper")]
#[cfg_attr(docsrs, doc(cfg(feature = "hyper")))]
pub fn listen(&self) -> crate::hyper::FutureTwitterStream {
let conn = hyper_tls::HttpsConnector::new();
self.listen_with_client(hyper_pkg::Client::builder().build::<_, hyper_pkg::Body>(conn))
}
pub fn listen_with_client<S, B>(&self, mut client: S) -> FutureTwitterStream<S::Future>
where
S: HttpService<B>,
B: From<Vec<u8>>,
{
let req = prepare_request(
self.endpoint.as_ref(),
self.token.as_ref(),
&self.parameters,
);
let response = client.call(req.map(Into::into));
FutureTwitterStream { response }
}
}
impl<'a, C, A> Builder<'a, Token<C, A>> {
pub fn endpoint(&mut self, endpoint: impl Into<Option<(RequestMethod, Uri)>>) -> &mut Self {
self.endpoint = endpoint.into();
self
}
pub fn token(&mut self, token: Token<C, A>) -> &mut Self {
self.token = token;
self
}
pub fn stall_warnings(&mut self, stall_warnings: bool) -> &mut Self {
self.parameters.stall_warnings = stall_warnings;
self
}
pub fn filter_level(&mut self, filter_level: impl Into<Option<FilterLevel>>) -> &mut Self {
self.parameters.filter_level = filter_level.into();
self
}
pub fn language(&mut self, language: impl Into<Cow<'a, str>>) -> &mut Self {
self.parameters.language = language.into();
self
}
pub fn follow(&mut self, follow: impl Into<Cow<'a, [u64]>>) -> &mut Self {
self.parameters.follow = follow.into();
self
}
pub fn track(&mut self, track: impl Into<Cow<'a, str>>) -> &mut Self {
self.parameters.track = track.into();
self
}
pub fn locations(&mut self, locations: impl Into<Cow<'a, [BoundingBox]>>) -> &mut Self {
self.parameters.locations = locations.into();
self
}
pub fn count(&mut self, count: impl Into<Option<i32>>) -> &mut Self {
self.parameters.count = count.into();
self
}
}
impl std::default::Default for FilterLevel {
fn default() -> Self {
FilterLevel::None
}
}
impl std::fmt::Display for FilterLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
AsRef::<str>::as_ref(self).fmt(f)
}
}
fn prepare_request(
endpoint: Option<&(RequestMethod, Uri)>,
token: Token<&str, &str>,
parameters: &Parameters<'_>,
) -> http::Request<Vec<u8>> {
let uri;
let (method, endpoint) = if let Some(&(ref method, ref endpoint)) = endpoint {
(method, endpoint)
} else if parameters.follow.is_empty()
&& parameters.track.is_empty()
&& parameters.locations.is_empty()
{
uri = Uri::from_static(SAMPLE);
(&RequestMethod::GET, &uri)
} else {
uri = Uri::from_static(FILTER);
(&RequestMethod::POST, &uri)
};
let req = Request::builder().method(method.clone());
let mut oauth = oauth::Builder::new(token.client.as_ref(), oauth::HmacSha1);
oauth.token(token.token.as_ref());
if RequestMethod::POST == method {
let authorization = oauth.post(endpoint, parameters);
let data = oauth::to_form_urlencoded(parameters);
req.uri(endpoint.clone())
.header(AUTHORIZATION, authorization)
.header(
CONTENT_TYPE,
HeaderValue::from_static("application/x-www-form-urlencoded"),
)
.header(CONTENT_LENGTH, data.len())
.body(data.into_bytes())
.unwrap()
} else {
let authorization = oauth.build(method.as_ref(), endpoint, parameters);
let uri = oauth::to_uri_query(endpoint.to_string(), parameters);
req.uri(uri)
.header(AUTHORIZATION, authorization)
.body(Vec::default())
.unwrap()
}
}
const COMMA: &str = "%2C";
fn fmt_follow(ids: &[u64], f: &mut Formatter<'_>) -> fmt::Result {
fmt_join(ids, COMMA, f)
}
fn fmt_locations(locs: &[BoundingBox], f: &mut Formatter<'_>) -> fmt::Result {
fmt_join(BoundingBox::flatten_slice(locs).flat(), COMMA, f)
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn not(p: &bool) -> bool {
!p
}