use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::vec::IntoIter as VecIter;
use chrono;
use futures::Stream;
use serde::{Deserialize, Serialize};
use crate::common::*;
use crate::{auth, entities, error, links, tweet};
mod fun;
mod raw;
pub use self::fun::*;
#[derive(Debug, Clone, derive_more::From)]
pub enum UserID {
ID(u64),
ScreenName(CowStr),
}
impl<'a> From<&'static str> for UserID {
fn from(name: &'static str) -> UserID {
UserID::ScreenName(name.into())
}
}
impl From<String> for UserID {
fn from(name: String) -> UserID {
UserID::ScreenName(name.into())
}
}
round_trip! { raw::RawTwitterUser,
#[derive(Debug, Clone)]
pub struct TwitterUser {
pub contributors_enabled: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
pub default_profile: bool,
pub default_profile_image: bool,
pub description: Option<String>,
pub entities: UserEntities,
pub favourites_count: i32,
pub follow_request_sent: Option<bool>,
pub followers_count: i32,
pub friends_count: i32,
pub geo_enabled: bool,
pub id: u64,
pub is_translator: bool,
pub lang: Option<String>,
pub listed_count: i32,
pub location: Option<String>,
pub name: String,
pub profile_background_color: String,
pub profile_background_image_url: Option<String>,
pub profile_background_image_url_https: Option<String>,
pub profile_background_tile: Option<bool>,
pub profile_banner_url: Option<String>,
pub profile_image_url: String,
pub profile_image_url_https: String,
pub profile_link_color: String,
pub profile_sidebar_border_color: String,
pub profile_sidebar_fill_color: String,
pub profile_text_color: String,
pub profile_use_background_image: bool,
pub protected: bool,
pub screen_name: String,
pub show_all_inline_media: Option<bool>,
pub status: Option<Box<tweet::Tweet>>,
pub statuses_count: i32,
pub time_zone: Option<String>,
pub url: Option<String>,
pub utc_offset: Option<i32>,
pub verified: bool,
pub withheld_in_countries: Option<Vec<String>>,
pub withheld_scope: Option<String>,
}
}
impl From<raw::RawTwitterUser> for TwitterUser {
fn from(mut raw: raw::RawTwitterUser) -> TwitterUser {
if let Some(ref description) = raw.description {
for entity in &mut raw.entities.description.urls {
codepoints_to_bytes(&mut entity.range, description);
}
}
if let (&mut Some(ref url), &mut Some(ref mut entities)) =
(&mut raw.url, &mut raw.entities.url)
{
for entity in &mut entities.urls {
codepoints_to_bytes(&mut entity.range, url);
}
}
TwitterUser {
contributors_enabled: raw.contributors_enabled,
created_at: raw.created_at,
default_profile: raw.default_profile,
default_profile_image: raw.default_profile_image,
description: raw.description,
entities: raw.entities,
favourites_count: raw.favourites_count,
follow_request_sent: raw.follow_request_sent,
followers_count: raw.followers_count,
friends_count: raw.friends_count,
geo_enabled: raw.geo_enabled,
id: raw.id,
is_translator: raw.is_translator,
lang: raw.lang,
listed_count: raw.listed_count,
location: raw.location,
name: raw.name,
profile_background_color: raw.profile_background_color,
profile_background_image_url: raw.profile_background_image_url,
profile_background_image_url_https: raw.profile_background_image_url_https,
profile_background_tile: raw.profile_background_tile,
profile_banner_url: raw.profile_banner_url,
profile_image_url: raw.profile_image_url,
profile_image_url_https: raw.profile_image_url_https,
profile_link_color: raw.profile_link_color,
profile_sidebar_border_color: raw.profile_sidebar_border_color,
profile_sidebar_fill_color: raw.profile_sidebar_fill_color,
profile_text_color: raw.profile_text_color,
profile_use_background_image: raw.profile_use_background_image,
protected: raw.protected,
screen_name: raw.screen_name,
show_all_inline_media: raw.show_all_inline_media,
status: raw.status,
statuses_count: raw.statuses_count,
time_zone: raw.time_zone,
url: raw.url,
utc_offset: raw.utc_offset,
verified: raw.verified,
withheld_in_countries: raw.withheld_in_countries,
withheld_scope: raw.withheld_scope,
}
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct UserEntities {
pub description: UserEntityDetail,
pub url: Option<UserEntityDetail>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct UserEntityDetail {
pub urls: Vec<entities::UrlEntity>,
}
#[must_use = "search iterators are lazy and do nothing unless consumed"]
pub struct UserSearch {
token: auth::Token,
query: CowStr,
pub page_num: i32,
pub page_size: i32,
current_loader: Option<FutureResponse<Vec<TwitterUser>>>,
current_results: Option<VecIter<TwitterUser>>,
}
impl UserSearch {
pub fn with_page_size(self, page_size: i32) -> Self {
UserSearch {
page_size,
current_loader: None,
current_results: None,
..self
}
}
pub fn start_at_page(self, page_num: i32) -> Self {
UserSearch {
page_num,
current_loader: None,
current_results: None,
..self
}
}
pub fn call(&self) -> impl Future<Output = error::Result<Response<Vec<TwitterUser>>>> {
let params = ParamList::new()
.add_param("q", self.query.clone())
.add_param("page", self.page_num.to_string())
.add_param("count", self.page_size.to_string());
let req = get(links::users::SEARCH, &self.token, Some(¶ms));
request_with_json_response(req)
}
fn new<S: Into<CowStr>>(query: S, token: &auth::Token) -> UserSearch {
UserSearch {
token: token.clone(),
query: query.into(),
page_num: 1,
page_size: 10,
current_loader: None,
current_results: None,
}
}
}
impl Stream for UserSearch {
type Item = Result<TwitterUser, error::Error>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
if let Some(mut fut) = self.current_loader.take() {
match Pin::new(&mut fut).poll(cx) {
Poll::Pending => {
self.current_loader = Some(fut);
return Poll::Pending;
}
Poll::Ready(Ok(res)) => self.current_results = Some(res.response.into_iter()),
Poll::Ready(Err(e)) => {
self.current_results = None;
return Poll::Ready(Some(Err(e)));
}
}
}
if let Some(ref mut results) = self.current_results {
if let Some(user) = results.next() {
return Poll::Ready(Some(Ok(user)));
} else if (results.len() as i32) < self.page_size {
return Poll::Ready(None);
} else {
self.page_num += 1;
}
}
self.current_loader = Some(Box::pin(self.call()));
self.poll_next(cx)
}
}
#[derive(Debug, Deserialize)]
pub struct Relationship {
pub target: RelationTarget,
pub source: RelationSource,
}
#[derive(Debug, Deserialize)]
pub struct RelationTarget {
pub id: u64,
pub screen_name: String,
pub followed_by: bool,
pub following: bool,
}
#[derive(Debug, Deserialize)]
pub struct RelationSource {
pub id: u64,
pub screen_name: String,
pub following: bool,
pub followed_by: bool,
pub can_dm: bool,
pub blocking: Option<bool>,
pub marked_spam: Option<bool>,
all_replies: Option<bool>,
pub want_retweets: Option<bool>,
pub notifications_enabled: Option<bool>,
}
#[derive(Debug, Deserialize)]
pub struct RelationLookup {
pub name: String,
pub screen_name: String,
pub id: u64,
pub connections: Vec<Connection>,
}
#[derive(Debug, Deserialize)]
pub enum Connection {
#[serde(rename = "none")]
None,
#[serde(rename = "following_requested")]
FollowingRequested,
#[serde(rename = "following_received")]
FollowingReceived,
#[serde(rename = "followed_by")]
FollowedBy,
#[serde(rename = "following")]
Following,
#[serde(rename = "blocking")]
Blocking,
#[serde(rename = "muting")]
Muting,
}
#[cfg(test)]
mod tests {
use super::TwitterUser;
use crate::common::tests::load_file;
#[test]
fn roundtrip_deser() {
let sample = load_file("sample_payloads/user_array.json");
let users_src: Vec<TwitterUser> = serde_json::from_str(&sample).unwrap();
let json1 = serde_json::to_value(users_src).unwrap();
let users_roundtrip: Vec<TwitterUser> = serde_json::from_value(json1.clone()).unwrap();
let json2 = serde_json::to_value(users_roundtrip).unwrap();
assert_eq!(json1, json2);
}
}