egg_mode/
service.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5//! Methods to inquire about the Twitter service itself.
6//!
7//! The functions included in this module are supplementary queries that are less about specific
8//! actions, and more about your interaction with the Twitter service as a whole. For example, this
9//! module includes methods to load the [Terms of Service][terms] or [Privacy Policy][privacy], or
10//! to ask about many methods' [rate-limit status][] or receive information about [various
11//! configuration elements][config] for broad service-level values. All the structs and enums
12//! contained in this module are connected to one of these methods.
13//!
14//! [terms]: fn.terms.html
15//! [privacy]: fn.privacy.html
16//! [rate-limit status]: fn.rate_limit_status.html
17//! [config]: fn.config.html
18
19use std::collections::HashMap;
20use std::result::Result as StdResult;
21use std::str::FromStr;
22
23use serde::de::Error;
24use serde::{Deserialize, Deserializer};
25use serde_json;
26
27use crate::common::*;
28use crate::error::{
29    Error::{InvalidResponse, MissingValue},
30    Result,
31};
32use crate::{auth, entities, links};
33
34///Returns a future that resolves to the current Twitter Terms of Service as plain text.
35///
36///While the official home of Twitter's TOS is <https://twitter.com/tos>, this allows you to obtain a
37///plain-text copy of it to display in your application.
38pub async fn terms(token: &auth::Token) -> Result<Response<String>> {
39    let req = get(links::service::TERMS, token, None);
40
41    let ret = request_with_json_response::<serde_json::Value>(req).await?;
42
43    let tos = ret
44        .response
45        .get("tos")
46        .and_then(|tos| tos.as_str())
47        .map(String::from)
48        .ok_or(InvalidResponse("Missing field: tos", None))?;
49    Ok(Response::map(ret, |_| tos))
50}
51
52///Returns a future that resolves to the current Twitter Privacy Policy as plain text.
53///
54///While the official home of Twitter's Privacy Policy is <https://twitter.com/privacy>, this allows
55///you to obtain a plain-text copy of it to display in your application.
56pub async fn privacy(token: &auth::Token) -> Result<Response<String>> {
57    let req = get(links::service::PRIVACY, token, None);
58
59    let ret = request_with_json_response::<serde_json::Value>(req).await?;
60
61    let privacy = ret
62        .response
63        .get("privacy")
64        .and_then(|tos| tos.as_str())
65        .map(String::from)
66        .ok_or(InvalidResponse("Missing field: privacy", None))?;
67    Ok(Response::map(ret, |_| privacy))
68}
69
70///Returns a future that resolves to the current configuration from Twitter, including the maximum
71///length of a t.co URL and maximum photo resolutions per size, among others.
72///
73///From Twitter: "It is recommended applications request this endpoint when they are loaded, but no
74///more than once a day."
75///
76///See the documentation for the [`Configuration`][] struct for a discussion of what individual
77///fields returned by this function mean.
78///
79///[`Configuration`]: struct.Configuration.html
80pub async fn config(token: &auth::Token) -> Result<Response<Configuration>> {
81    let req = get(links::service::CONFIG, token, None);
82    request_with_json_response(req).await
83}
84
85///Return the current rate-limit status for all available methods from the authenticated user.
86///
87///The struct returned by this method is organized by what module in egg-mode a given method
88///appears in. Note that not every method's status is available through this method; see the
89///documentation for [`RateLimitStatus`][] and its associated enums for more information.
90///
91///[`RateLimitStatus`]: struct.RateLimitStatus.html
92pub async fn rate_limit_status(token: &auth::Token) -> Result<Response<RateLimitStatus>> {
93    let req = get(links::service::RATE_LIMIT_STATUS, token, None);
94    request_with_json_response(req).await
95}
96
97///Like `rate_limit_status`, but returns the raw JSON without processing it. Only intended to
98///return the full structure so that new methods can be added to `RateLimitStatus` and its
99///associated enums.
100#[doc(hidden)]
101pub async fn rate_limit_status_raw(token: &auth::Token) -> Result<Response<serde_json::Value>> {
102    let req = get(links::service::RATE_LIMIT_STATUS, token, None);
103    request_with_json_response(req).await
104}
105
106///Represents a service configuration from Twitter.
107///
108///The values returned in this struct are various pieces of information that, while they don't
109///change often, have the opportunity to change over time and affect things like character counting
110///or whether to route a twitter.com URL to a user lookup or a browser.
111///
112///While tweets themselves still have a fixed 280-character limit, direct messages have had their
113///text limit expanded to 10,000 characters, and that length is communicated here, in
114///`dm_text_character_limit`.
115///
116///For `photo_sizes`, note that if your image is smaller than the dimensions given for a particular
117///size, that size variant will simply return your source image as-is. If either dimension is
118///larger than its corresponding dimension here, it will be scaled according to the included
119///`resize` property. In practice this usually means `thumb` will crop to its dimensions, and each
120///other variant will resize down, keeping its aspect ratio.
121///
122///For best ways to handle the `short_url_length` fields, see Twitter's documentation on [t.co
123///URLs][]. In short, every URL Twitter detects in a new tweet or direct message gets a new t.co
124///URL created for it, which replaces the original URL in the given text. This affects character
125///counts for these fields, so if your app is counting characters and detects a URL for these
126///fields, treat the whole URL as if it were as long as the number of characters given in this
127///struct.
128///
129///[t.co URLs]: https://developer.twitter.com/en/docs/basics/tco
130///
131///Finally, loading `non_username_paths` allows you to handle `twitter.com/[name]` links as if they
132///were a user mention, while still keeping site-level links working properly.
133#[derive(Debug, Deserialize)]
134pub struct Configuration {
135    ///The character limit in direct messages.
136    pub dm_text_character_limit: i32,
137    ///The maximum dimensions for each photo size variant.
138    pub photo_sizes: entities::MediaSizes,
139    ///The maximum length for a t.co URL when given a URL with protocol `http`.
140    pub short_url_length: i32,
141    ///The maximum length for a t.co URL when given a URL with protocol `https`.
142    pub short_url_length_https: i32,
143    ///A list of URL slugs that are not valid usernames when in a URL like `twitter.com/[slug]`.
144    pub non_username_paths: Vec<String>,
145}
146
147/// Represents the current rate-limit status of many Twitter API calls.
148///
149/// This is organized by module, so for example, if you wanted to see your rate-limit status for
150/// `tweet::home_timeline`, you could access it like this:
151///
152/// ```rust,no_run
153/// # use egg_mode::Token;
154/// # #[tokio::main]
155/// # async fn main() {
156/// # let token: Token = unimplemented!();
157/// # let status = egg_mode::service::rate_limit_status(&token).await.unwrap();
158/// use egg_mode::service::TweetMethod;
159/// println!("home_timeline calls remaining: {}",
160///          status.tweet[&TweetMethod::HomeTimeline].rate_limit_status.remaining);
161/// # }
162/// ```
163///
164/// It's important to note that not every API method is available through this call. Namely, most
165/// calls that require a POST under-the-hood (those that add or modify data with the Twitter
166/// service) are not shown through this method. For a listing of methods available for rate-limit
167/// querying, see the `*Method` enums available in [`egg_mode::service`][].
168///
169/// [`egg_mode::service`]: index.html
170#[derive(Debug)]
171pub struct RateLimitStatus {
172    ///The rate-limit status for methods in the `direct` module.
173    pub direct: HashMap<DirectMethod, Response<()>>,
174    ///The rate-limit status for methods in the `place` module.
175    pub place: HashMap<PlaceMethod, Response<()>>,
176    ///The rate-limit status for methods in the `search` module.
177    pub search: HashMap<SearchMethod, Response<()>>,
178    ///The rate-limit status for methods in the `service` module.
179    pub service: HashMap<ServiceMethod, Response<()>>,
180    ///The rate-limit status for methods in the `tweet` module.
181    pub tweet: HashMap<TweetMethod, Response<()>>,
182    ///The rate-limit status for methods in the `user` module.
183    pub user: HashMap<UserMethod, Response<()>>,
184    ///The rate-limit status for methods in the `list` module.
185    pub list: HashMap<ListMethod, Response<()>>,
186}
187
188impl<'de> Deserialize<'de> for RateLimitStatus {
189    fn deserialize<D>(ser: D) -> StdResult<Self, D::Error>
190    where
191        D: Deserializer<'de>,
192    {
193        use serde_json::from_value;
194
195        let input = serde_json::Value::deserialize(ser)?;
196
197        let mut direct = HashMap::new();
198        let mut place = HashMap::new();
199        let mut search = HashMap::new();
200        let mut service = HashMap::new();
201        let mut tweet = HashMap::new();
202        let mut user = HashMap::new();
203        let mut list = HashMap::new();
204
205        let map = input
206            .get("resources")
207            .ok_or_else(|| D::Error::custom(MissingValue("resources")))?;
208
209        if let Some(map) = map.as_object() {
210            for (k, v) in map
211                .values()
212                .filter_map(|v| v.as_object())
213                .flat_map(|v| v.iter())
214            {
215                if let Ok(method) = k.parse::<Method>() {
216                    match method {
217                        Method::Direct(m) => {
218                            direct.insert(m, from_value(v.clone()).map_err(D::Error::custom)?)
219                        }
220                        Method::Place(p) => {
221                            place.insert(p, from_value(v.clone()).map_err(D::Error::custom)?)
222                        }
223                        Method::Search(s) => {
224                            search.insert(s, from_value(v.clone()).map_err(D::Error::custom)?)
225                        }
226                        Method::Service(s) => {
227                            service.insert(s, from_value(v.clone()).map_err(D::Error::custom)?)
228                        }
229                        Method::Tweet(t) => {
230                            tweet.insert(t, from_value(v.clone()).map_err(D::Error::custom)?)
231                        }
232                        Method::User(u) => {
233                            user.insert(u, from_value(v.clone()).map_err(D::Error::custom)?)
234                        }
235                        Method::List(l) => {
236                            list.insert(l, from_value(v.clone()).map_err(D::Error::custom)?)
237                        }
238                    };
239                }
240            }
241        } else {
242            return Err(D::Error::custom(InvalidResponse(
243                "RateLimitStatus field 'resources' wasn't an object",
244                Some(input.to_string()),
245            )));
246        }
247
248        Ok(RateLimitStatus {
249            direct,
250            place,
251            search,
252            service,
253            tweet,
254            user,
255            list,
256        })
257    }
258}
259
260///Method identifiers, used by `rate_limit_status` to return rate-limit information.
261enum Method {
262    ///A method from the `direct` module.
263    Direct(DirectMethod),
264    ///A method from the `place` module.
265    Place(PlaceMethod),
266    ///A method from the `search` module.
267    Search(SearchMethod),
268    ///A method from the `service` module.
269    Service(ServiceMethod),
270    ///A method from the `tweet` module.
271    Tweet(TweetMethod),
272    ///A method from the `user` module.
273    User(UserMethod),
274    ///A method from the `list` module.
275    List(ListMethod),
276}
277
278impl FromStr for Method {
279    type Err = ();
280
281    fn from_str(s: &str) -> StdResult<Self, ()> {
282        match s {
283            "/direct_messages" => Ok(Method::Direct(DirectMethod::Received)),
284            "/direct_messages/sent" => Ok(Method::Direct(DirectMethod::Sent)),
285            "/direct_messages/show" => Ok(Method::Direct(DirectMethod::Show)),
286
287            "/geo/search" => Ok(Method::Place(PlaceMethod::Search)),
288            "/geo/reverse_geocode" => Ok(Method::Place(PlaceMethod::ReverseGeocode)),
289            "/geo/id/:place_id" => Ok(Method::Place(PlaceMethod::Show)),
290
291            "/search/tweets" => Ok(Method::Search(SearchMethod::Search)),
292
293            "/help/configuration" => Ok(Method::Service(ServiceMethod::Config)),
294            "/help/privacy" => Ok(Method::Service(ServiceMethod::Privacy)),
295            "/help/tos" => Ok(Method::Service(ServiceMethod::Terms)),
296            "/account/verify_credentials" => Ok(Method::Service(ServiceMethod::VerifyTokens)),
297            "/application/rate_limit_status" => Ok(Method::Service(ServiceMethod::RateLimitStatus)),
298
299            "/statuses/mentions_timeline" => Ok(Method::Tweet(TweetMethod::MentionsTimeline)),
300            "/statuses/user_timeline" => Ok(Method::Tweet(TweetMethod::UserTimeline)),
301            "/statuses/home_timeline" => Ok(Method::Tweet(TweetMethod::HomeTimeline)),
302            "/statuses/retweets_of_me" => Ok(Method::Tweet(TweetMethod::RetweetsOfMe)),
303            "/statuses/retweets/:id" => Ok(Method::Tweet(TweetMethod::RetweetsOf)),
304            "/statuses/show/:id" => Ok(Method::Tweet(TweetMethod::Show)),
305            "/statuses/retweeters/ids" => Ok(Method::Tweet(TweetMethod::RetweetersOf)),
306            "/statuses/lookup" => Ok(Method::Tweet(TweetMethod::Lookup)),
307            "/favorites/list" => Ok(Method::Tweet(TweetMethod::LikedBy)),
308
309            "/users/show/:id" => Ok(Method::User(UserMethod::Show)),
310            "/users/lookup" => Ok(Method::User(UserMethod::Lookup)),
311            "/users/search" => Ok(Method::User(UserMethod::Search)),
312            "/friends/list" => Ok(Method::User(UserMethod::FriendsOf)),
313            "/friends/ids" => Ok(Method::User(UserMethod::FriendsIds)),
314            "/friendships/incoming" => Ok(Method::User(UserMethod::IncomingRequests)),
315            "/friendships/outgoing" => Ok(Method::User(UserMethod::OutgoingRequests)),
316            "/friendships/no_retweets/ids" => Ok(Method::User(UserMethod::FriendsNoRetweets)),
317            "/followers/list" => Ok(Method::User(UserMethod::FollowersOf)),
318            "/followers/ids" => Ok(Method::User(UserMethod::FollowersIds)),
319            "/blocks/list" => Ok(Method::User(UserMethod::Blocks)),
320            "/blocks/ids" => Ok(Method::User(UserMethod::BlocksIds)),
321            "/users/report_spam" => Ok(Method::User(UserMethod::ReportSpam)),
322            "/mutes/users/list" => Ok(Method::User(UserMethod::Mutes)),
323            "/mutes/users/ids" => Ok(Method::User(UserMethod::MutesIds)),
324            "/friendships/show" => Ok(Method::User(UserMethod::Relation)),
325            "/friendships/lookup" => Ok(Method::User(UserMethod::RelationLookup)),
326
327            "/lists/show" => Ok(Method::List(ListMethod::Show)),
328            "/lists/ownerships" => Ok(Method::List(ListMethod::Ownerships)),
329            "/lists/subscriptions" => Ok(Method::List(ListMethod::Subscriptions)),
330            "/lists/list" => Ok(Method::List(ListMethod::List)),
331            "/lists/members" => Ok(Method::List(ListMethod::Members)),
332            "/lists/memberships" => Ok(Method::List(ListMethod::Memberships)),
333            "/lists/members/show" => Ok(Method::List(ListMethod::IsMember)),
334            "/lists/subscribers" => Ok(Method::List(ListMethod::Subscribers)),
335            "/lists/subscribers/show" => Ok(Method::List(ListMethod::IsSubscribed)),
336            "/lists/statuses" => Ok(Method::List(ListMethod::Statuses)),
337
338            _ => Err(()),
339        }
340    }
341}
342
343///Method identifiers from the `direct` module, for use by `rate_limit_status`.
344#[derive(Debug, PartialEq, Eq, Hash)]
345pub enum DirectMethod {
346    ///`direct::show`
347    Show,
348    ///`direct::sent`
349    Sent,
350    ///`direct::received`
351    Received,
352}
353
354///Method identifiers from the `place` module, for use by `rate_limit_status`.
355#[derive(Debug, PartialEq, Eq, Hash)]
356pub enum PlaceMethod {
357    ///`place::show`
358    Show,
359    ///`place::search_point`, `place::search_query`, `place::search_ip` and `place::search_url`
360    Search,
361    ///`place::reverse_geocode` and `place::reverse_geocode_url`
362    ReverseGeocode,
363}
364
365///Method identifiers from the `search` module, for use by `rate_limit_status`.
366#[derive(Debug, PartialEq, Eq, Hash)]
367pub enum SearchMethod {
368    ///`search::search`
369    Search,
370}
371
372///Method identifiers from the `service` module, for use by `rate_limit_status`. Also includes
373///`verify_tokens` from the egg-mode top-level methods.
374#[derive(Debug, PartialEq, Eq, Hash)]
375pub enum ServiceMethod {
376    ///`service::terms`
377    Terms,
378    ///`service::privacy`
379    Privacy,
380    ///`service::config`
381    Config,
382    ///`service::rate_limit_status`
383    RateLimitStatus,
384    ///`verify_tokens`
385    VerifyTokens,
386}
387
388///Method identifiers from the `tweet` module, for use by `rate_limit_status`.
389#[derive(Debug, PartialEq, Eq, Hash)]
390pub enum TweetMethod {
391    ///`tweet::show`
392    Show,
393    ///`tweet::lookup`
394    Lookup,
395    ///`tweet::retweeters_of`
396    RetweetersOf,
397    ///`tweet::retweets_of`
398    RetweetsOf,
399    ///`tweet::home_timeline`
400    HomeTimeline,
401    ///`tweet::mentions_timeline`
402    MentionsTimeline,
403    ///`tweet::user_timeline`
404    UserTimeline,
405    ///`tweet::retweets_of_me`
406    RetweetsOfMe,
407    ///`tweet::liked_by`
408    LikedBy,
409}
410
411///Method identifiers from the `user` module, for use by `rate_limit_status`.
412#[derive(Debug, PartialEq, Eq, Hash)]
413pub enum UserMethod {
414    ///`user::show`
415    Show,
416    ///`user::lookup`
417    Lookup,
418    ///`user::friends_no_retweets`
419    FriendsNoRetweets,
420    ///`user::relation`
421    Relation,
422    ///`user::relation_lookup`
423    RelationLookup,
424    ///`user::search`
425    Search,
426    ///`user::friends_of`
427    FriendsOf,
428    ///`user::friends_ids`
429    FriendsIds,
430    ///`user::followers_of`
431    FollowersOf,
432    ///`user::followers_ids`
433    FollowersIds,
434    ///`user::blocks`
435    Blocks,
436    ///`user::blocks_ids`
437    BlocksIds,
438    ///`user::mutes`
439    Mutes,
440    ///`user::mutes_ids`
441    MutesIds,
442    ///`user::incoming_requests`
443    IncomingRequests,
444    ///`user::outgoing_requests`
445    OutgoingRequests,
446    ///`user::report_spam`
447    ReportSpam,
448}
449
450///Method identifiers from the `list` module, for use by `rate_limit_status`.
451#[derive(Debug, PartialEq, Eq, Hash)]
452pub enum ListMethod {
453    ///`list::show`
454    Show,
455    ///`list::ownerships`
456    Ownerships,
457    ///`list::subscriptions`
458    Subscriptions,
459    ///`list::list`
460    List,
461    ///`list::members`
462    Members,
463    ///`list::memberships`
464    Memberships,
465    ///`list::is_member`
466    IsMember,
467    ///`list::subscribers`
468    Subscribers,
469    ///`list::is_subscribed`
470    IsSubscribed,
471    ///`list::statuses`
472    Statuses,
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478    use crate::common::tests::load_file;
479
480    #[test]
481    fn parse_rate_limit() {
482        let sample = load_file("sample_payloads/rate_limit_sample.json");
483        ::serde_json::from_str::<RateLimitStatus>(&sample).unwrap();
484    }
485}