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}