1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
// 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 std::collections::HashMap;

use crate::common::*;
use crate::error::{Error::InvalidResponse, Result};
use crate::user::UserID;
use crate::{auth, cursor, links};
use serde_json;

use super::*;

///Lookup a single tweet by numeric ID.
pub async fn show(id: u64, token: &auth::Token) -> Result<Response<Tweet>> {
    let params = ParamList::new()
        .extended_tweets()
        .add_param("id", id.to_string())
        .add_param("include_my_retweet", "true")
        .add_param("include_ext_alt_text", "true");
    let req = get(links::statuses::SHOW, token, Some(&params));
    request_with_json_response(req).await
}

///Lookup the most recent 100 (or fewer) retweets of the given tweet.
///
///Use the `count` parameter to indicate how many retweets you would like to retrieve. If `count`
///is 0 or greater than 100, it will be defaulted to 100 before making the call.
pub async fn retweets_of(id: u64, count: u32, token: &auth::Token) -> Result<Response<Vec<Tweet>>> {
    let params = ParamList::new().extended_tweets().add_param(
        "count",
        if count == 0 || count > 100 {
            100
        } else {
            count
        }
        .to_string(),
    );

    let url = format!("{}/{}.json", links::statuses::RETWEETS_OF_STEM, id);
    let req = get(&url, token, Some(&params));
    request_with_json_response(req).await
}

///Lookup the user IDs that have retweeted the given tweet.
///
///Note that while loading the list of retweeters is a cursored search, it does not allow you to
///set the page size. Calling `with_page_size` on the iterator returned by this function will not
///change the page size used by the network call. Setting `page_size` manually may result in an
///error from Twitter.
pub fn retweeters_of(id: u64, token: &auth::Token) -> cursor::CursorIter<cursor::IDCursor> {
    let params = ParamList::new().add_param("id", id.to_string());
    cursor::CursorIter::new(links::statuses::RETWEETERS_OF, token, Some(params), None)
}

///Lookup tweet information for the given list of tweet IDs.
///
///This function differs from `lookup_map` in how it handles protected or nonexistent tweets.
///`lookup` gives a Vec of just the tweets it could load, leaving out any that it couldn't find.
pub async fn lookup<I: IntoIterator<Item = u64>>(
    ids: I,
    token: &auth::Token,
) -> Result<Response<Vec<Tweet>>> {
    let id_param = ids.into_iter().fold(String::new(), |mut acc, x| {
        if !acc.is_empty() {
            acc.push(',');
        }
        acc.push_str(&x.to_string());
        acc
    });
    let params = ParamList::new()
        .extended_tweets()
        .add_param("id", id_param)
        .add_param("include_ext_alt_text", "true");

    let req = post(links::statuses::LOOKUP, token, Some(&params));
    request_with_json_response(req).await
}

///Lookup tweet information for the given list of tweet IDs, and return a map indicating which IDs
///couldn't be found.
///
///This function differs from `lookup` in how it handles protected or nonexistent tweets.
///`lookup_map` gives a map containing every ID in the input slice; tweets that don't exist or
///can't be read by the authenticated user store `None` in the map, whereas tweets that could be
///loaded store `Some` and the requested status.
pub async fn lookup_map<I: IntoIterator<Item = u64>>(
    ids: I,
    token: &auth::Token,
) -> Result<Response<HashMap<u64, Option<Tweet>>>> {
    let id_param = ids.into_iter().fold(String::new(), |mut acc, x| {
        if !acc.is_empty() {
            acc.push(',');
        }
        acc.push_str(&x.to_string());
        acc
    });
    let params = ParamList::new()
        .extended_tweets()
        .add_param("id", id_param)
        .add_param("map", "true")
        .add_param("include_ext_alt_text", "true");

    let req = post(links::statuses::LOOKUP, token, Some(&params));
    let parsed = request_with_json_response::<serde_json::Value>(req).await?;
    let mut map = HashMap::new();

    for (key, val) in parsed
        .response
        .get("id")
        .and_then(|v| v.as_object())
        .ok_or_else(|| {
            InvalidResponse(
                "unexpected response for lookup_map",
                Some(parsed.response.to_string()),
            )
        })?
    {
        let id = key
            .parse::<u64>()
            .map_err(|_| InvalidResponse("could not parse id as integer", Some(key.to_string())))?;
        if val.is_null() {
            map.insert(id, None);
        } else {
            let tweet = Tweet::deserialize(val)?;
            map.insert(id, Some(tweet));
        }
    }

    Ok(Response::map(parsed, |_| map))
}

///Make a `Timeline` struct for navigating the collection of tweets posted by the authenticated
///user and the users they follow.
///
///This method has a default page size of 20 tweets, with a maximum of 200.
///
///Twitter will only return the most recent 800 tweets by navigating this method.
pub fn home_timeline(token: &auth::Token) -> Timeline {
    Timeline::new(links::statuses::HOME_TIMELINE, None, token)
}

///Make a `Timeline` struct for navigating the collection of tweets that mention the authenticated
///user's screen name.
///
///This method has a default page size of 20 tweets, with a maximum of 200.
///
///Twitter will only return the most recent 800 tweets by navigating this method.
pub fn mentions_timeline(token: &auth::Token) -> Timeline {
    Timeline::new(links::statuses::MENTIONS_TIMELINE, None, token)
}

///Make a `Timeline` struct for navigating the collection of tweets posted by the given user,
///optionally including or excluding replies or retweets.
///
///Attempting to load the timeline of a protected account will only work if the account is the
///authenticated user's, or if the authenticated user is an approved follower of the account.
///
///This method has a default page size of 20 tweets, with a maximum of 200. Note that asking to
///leave out replies or retweets will generate pages that may have fewer tweets than your requested
///page size; Twitter will load the requested number of tweets before removing replies and/or
///retweets.
///
///Twitter will only load the most recent 3,200 tweets with this method.
pub fn user_timeline<T: Into<UserID>>(
    acct: T,
    with_replies: bool,
    with_rts: bool,
    token: &auth::Token,
) -> Timeline {
    let params = ParamList::new()
        .extended_tweets()
        .add_user_param(acct.into())
        .add_param("exclude_replies", (!with_replies).to_string())
        .add_param("include_rts", with_rts.to_string());

    Timeline::new(links::statuses::USER_TIMELINE, Some(params), token)
}

///Make a `Timeline` struct for navigating the collection of tweets posted by the authenticated
///user that have been retweeted by others.
///
///This method has a default page size of 20 tweets, with a maximum of 100.
pub fn retweets_of_me(token: &auth::Token) -> Timeline {
    Timeline::new(links::statuses::RETWEETS_OF_ME, None, token)
}

///Make a `Timeline` struct for navigating the collection of tweets liked by the given user.
///
///This method has a default page size of 20 tweets, with a maximum of 200.
pub fn liked_by<T: Into<UserID>>(acct: T, token: &auth::Token) -> Timeline {
    let params = ParamList::new()
        .extended_tweets()
        .add_user_param(acct.into());
    Timeline::new(links::statuses::LIKES_OF, Some(params), token)
}

///Retweet the given status as the authenticated user.
///
///On success, the future returned by this function yields the retweet, with the original status
///contained in `retweeted_status`.
pub async fn retweet(id: u64, token: &auth::Token) -> Result<Response<Tweet>> {
    let params = ParamList::new().extended_tweets();
    let url = format!("{}/{}.json", links::statuses::RETWEET_STEM, id);
    let req = post(&url, token, Some(&params));
    request_with_json_response(req).await
}

///Unretweet the given status as the authenticated user.
///
///The given ID may either be the original status, or the ID of the authenticated user's retweet of
///it.
///
///On success, the future returned by this function yields the original tweet.
pub async fn unretweet(id: u64, token: &auth::Token) -> Result<Response<Tweet>> {
    let params = ParamList::new().extended_tweets();
    let url = format!("{}/{}.json", links::statuses::UNRETWEET_STEM, id);
    let req = post(&url, token, Some(&params));
    request_with_json_response(req).await
}

///Like the given status as the authenticated user.
///
///On success, the future returned by this function yields the liked tweet.
pub async fn like(id: u64, token: &auth::Token) -> Result<Response<Tweet>> {
    let params = ParamList::new()
        .extended_tweets()
        .add_param("id", id.to_string());
    let req = post(links::statuses::LIKE, token, Some(&params));
    request_with_json_response(req).await
}

///Clears a like of the given status as the authenticated user.
///
///On success, the future returned by this function yields the given tweet.
pub async fn unlike(id: u64, token: &auth::Token) -> Result<Response<Tweet>> {
    let params = ParamList::new()
        .extended_tweets()
        .add_param("id", id.to_string());
    let req = post(links::statuses::UNLIKE, token, Some(&params));
    request_with_json_response(req).await
}

///Delete the given tweet. The authenticated user must be the user who posted the given tweet.
///
///On success, the future returned by this function yields the given tweet.
pub async fn delete(id: u64, token: &auth::Token) -> Result<Response<Tweet>> {
    let params = ParamList::new().extended_tweets();
    let url = format!("{}/{}.json", links::statuses::DELETE_STEM, id);
    let req = post(&url, token, Some(&params));
    request_with_json_response(req).await
}