egg_mode/user/
mod.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//! Structs and methods for pulling user information from Twitter.
6//!
7//! Everything in here acts on users in some way, whether looking up user information, finding the
8//! relations between two users, or actions like following or blocking a user.
9//!
10//! ## Types
11//!
12//! - `UserID`: used as a generic input to many functions, this enum allows you to refer to a user
13//!   by a numeric ID or by their screen name.
14//! - `Relationship`/`RelationSource`/`RelationTarget`: returned by `relation`, these types
15//!   (`Relationship` contains the other two) show the ways two accounts relate to each other.
16//! - `RelationLookup`/`Connection`: returned as part of a collection by `relation_lookup`, these
17//!   types (`RelationLookup` contains a `Vec<Connection>`) shows the ways the authenticated user
18//!   relates to a specific account.
19//! - `TwitterUser`/`UserEntities`/`UserEntityDetail`: returned by many functions in this module,
20//!   these types (`TwitterUser` contains the other two) describe the content of a user's profile,
21//!   and a handful of settings relating to how their profile is displayed.
22//! - `UserSearch`: returned by `search`, this is a stream of search results.
23//!
24//! ## Functions
25//!
26//! ### User actions
27//!
28//! These functions perform actions to the user's account. Their use requires that your application
29//! request write access to authenticated accounts.
30//!
31//! - `block`/`report_spam`/`unblock`
32//! - `follow`/`unfollow`/`update_follow`
33//! - `mute`/`unmute`
34//!
35//! ### Direct lookup
36//!
37//! These functions return single users, or groups of users without having to iterate over the
38//! results.
39//!
40//! - `show`
41//! - `lookup`/`lookup_ids`/`lookup_names`
42//! - `friends_no_retweets`
43//! - `relation`/`relation_lookup`
44//!
45//! ### Cursored lookup
46//!
47//! These functions imply that they can return more entries than Twitter is willing to return at
48//! once, so they're delivered in pages. This library takes those paginated results and wraps a
49//! stream around them that loads the pages as-needed.
50//!
51//! - `search`
52//! - `friends_of`/`friends_ids`
53//! - `followers_of`/`followers_ids`
54//! - `blocks`/`blocks_ids`
55//! - `mutes`/`mutes_ids`
56//! - `incoming_requests`/`outgoing_requests`
57
58use std::future::Future;
59use std::pin::Pin;
60use std::task::{Context, Poll};
61use std::vec::IntoIter as VecIter;
62
63use chrono;
64use futures::Stream;
65use serde::{Deserialize, Serialize};
66
67use crate::common::*;
68use crate::{auth, entities, error, links, tweet};
69
70mod fun;
71mod raw;
72
73pub use self::fun::*;
74
75/// Convenience enum to generalize between referring to an account by numeric ID or by screen name.
76///
77/// Many API calls ask for a user either by either screen name (e.g. `rustlang`) or by a numeric ID
78/// assigned to the account (e.g. `165262228`). In egg-mode, these calls are abstracted around this
79/// enum, and can take any type that converts into it. This enum has `From` implementations for the
80/// following types:
81///
82/// * `u64`
83/// * `&u64` (convenient when used with iterators)
84/// * `&str`
85/// * `&&str` (convenient when used with iterators)
86/// * `&String` (to counteract the fact that deref coercion doesn't work with generics)
87/// * `&UserID` (convenient when used with iterators)
88///
89/// This way, when a function in egg-mode has a paremeter of type `T: Into<UserID>`, you can
90/// call it with any of these types, and it will be converted automatically. egg-mode will then use
91/// the proper parameter when performing the call to Twitter.
92#[derive(Debug, Clone, derive_more::From)]
93pub enum UserID {
94    /// Referring via the account's numeric ID.
95    ID(u64),
96    /// Referring via the account's screen name.
97    ScreenName(CowStr),
98}
99
100impl<'a> From<&'static str> for UserID {
101    fn from(name: &'static str) -> UserID {
102        UserID::ScreenName(name.into())
103    }
104}
105
106impl From<String> for UserID {
107    fn from(name: String) -> UserID {
108        UserID::ScreenName(name.into())
109    }
110}
111
112round_trip! { raw::RawTwitterUser,
113    /// Represents a Twitter user.
114    ///
115    /// Field-level documentation is mostly ripped wholesale from the previous
116    /// edition of [Twitter's user documentation][api-user].
117    ///
118    /// [api-user]: https://web.archive.org/web/20161114173522/https://dev.twitter.com/overview/api/users
119    ///
120    /// The fields present in this struct can be divided up into a few sections: Profile Information and
121    /// Settings.
122    ///
123    /// ## Profile Information
124    ///
125    /// Information here can be considered part of the user's profile. These fields are the "obvious"
126    /// visible portion of a profile view.
127    ///
128    /// * `id`
129    /// * `screen_name`
130    /// * `name`
131    /// * `verified`
132    /// * `protected`
133    /// * `description`
134    /// * `location`
135    /// * `url`
136    /// * `statuses_count`
137    /// * `friends_count`
138    /// * `followers_count`
139    /// * `favourites_count`
140    /// * `listed_count`
141    /// * `profile_image_url`/`profile_image_url_https`
142    /// * `profile_banner_url`
143    ///
144    /// ## Settings Information
145    ///
146    /// Information here can be used to alter the UI around this user, or to provide further metadata
147    /// that may not necessarily be user-facing.
148    ///
149    /// * `contributors_enabled`
150    /// * `created_at`
151    /// * `default_profile_image`
152    /// * `follow_request_sent`
153    /// * `default_profile`, `profile_background_color`, `profile_background_image_url`,
154    ///   `profile_background_image_url_https`, `profile_background_tile`, `profile_link_color`,
155    ///   `profile_sidebar_border_color`, `profile_sidebar_fill_color`, `profile_text_color`,
156    ///   `profile_use_background_image`: These fields can be used to theme a user's profile page to
157    ///   look like the settings they've set on the Twitter website.
158    /// * `geo_enabled`
159    /// * `is_translator`
160    /// * `lang`
161    /// * `show_all_inline_media`
162    /// * `time_zone`/`utc_offset`
163    /// * `withheld_in_countries`/`withheld_scope`
164    #[derive(Debug, Clone)]
165    pub struct TwitterUser {
166        /// Indicates this user has an account with "contributor mode" enabled, allowing
167        /// for Tweets issued by the user to be co-authored by another account. Rarely `true`.
168        pub contributors_enabled: bool,
169        /// The UTC timestamp for when this user account was created on Twitter.
170        pub created_at: chrono::DateTime<chrono::Utc>,
171        /// When true, indicates that this user has not altered the theme or background of
172        /// their user profile.
173        pub default_profile: bool,
174        /// When true, indicates that the user has not uploaded their own avatar and a default
175        /// egg avatar is used instead.
176        pub default_profile_image: bool,
177        /// The user-defined string describing their account.
178        pub description: Option<String>,
179        /// Link information that has been parsed out of the `url` or `description` fields given by the
180        /// user.
181        pub entities: UserEntities,
182        /// The number of tweets this user has favorited or liked in the account's lifetime.
183        /// The term "favourites" and its British spelling are used for historical reasons.
184        pub favourites_count: i32,
185        /// When true, indicates that the authenticating user has issued a follow request to
186        /// this protected account.
187        pub follow_request_sent: Option<bool>,
188        /// The number of followers this account has.
189        ///
190        /// In certain server-stress conditions, this may temporarily mistakenly return 0.
191        pub followers_count: i32,
192        /// The number of users this account follows, aka its "followings".
193        ///
194        /// In certain server-stress conditions, this may temporarily mistakenly return 0.
195        pub friends_count: i32,
196        /// Indicates whether this user as enabled their tweets to be geotagged.
197        ///
198        /// If this is set for the current user, then they can attach geographic data when
199        /// posting a new Tweet.
200        pub geo_enabled: bool,
201        /// Unique identifier for this user.
202        pub id: u64,
203        /// Indicates whether the user participates in Twitter's translator community.
204        pub is_translator: bool,
205        /// Language code for the user's self-declared interface language.
206        ///
207        /// Codes are formatted as a language tag from [BCP 47][]. Only indicates the user's
208        /// interface language, not necessarily the content of their Tweets.
209        ///
210        /// [BCP 47]: https://tools.ietf.org/html/bcp47
211        pub lang: Option<String>,
212        /// The number of public lists the user is a member of.
213        pub listed_count: i32,
214        /// The user-entered location field from their profile. Not necessarily parseable
215        /// or even a location.
216        pub location: Option<String>,
217        /// The user-entered display name.
218        pub name: String,
219        /// The hex color chosen by the user for their profile background.
220        pub profile_background_color: String,
221        /// A URL pointing to the background image chosen by the user for their profile. Uses
222        /// HTTP as the protocol.
223        pub profile_background_image_url: Option<String>,
224        /// A URL pointing to the background image chosen by the user for their profile. Uses
225        /// HTTPS as the protocol.
226        pub profile_background_image_url_https: Option<String>,
227        /// Indicates whether the user's `profile_background_image_url` should be tiled when
228        /// displayed.
229        pub profile_background_tile: Option<bool>,
230        /// A URL pointing to the banner image chosen by the user. Uses HTTPS as the protocol.
231        ///
232        /// This is a base URL that a size specifier can be appended onto to get variously
233        /// sized images, with size specifiers according to [Profile Images and Banners][profile-img].
234        ///
235        /// [profile-img]: https://developer.twitter.com/en/docs/accounts-and-users/user-profile-images-and-banners
236        pub profile_banner_url: Option<String>,
237        /// A URL pointing to the user's avatar image. Uses HTTP as the protocol. Size
238        /// specifiers can be used according to [Profile Images and Banners][profile-img].
239        ///
240        /// [profile-img]: https://developer.twitter.com/en/docs/accounts-and-users/user-profile-images-and-banners
241        pub profile_image_url: String,
242        /// A URL pointing to the user's avatar image. Uses HTTPS as the protocol. Size
243        /// specifiers can be used according to [Profile Images and Banners][profile-img].
244        ///
245        /// [profile-img]: https://developer.twitter.com/en/docs/accounts-and-users/user-profile-images-and-banners
246        pub profile_image_url_https: String,
247        /// The hex color chosen by the user to display links in the Twitter UI.
248        pub profile_link_color: String,
249        /// The hex color chosen by the user to display sidebar borders in the Twitter UI.
250        pub profile_sidebar_border_color: String,
251        /// The hex color chosen by the user to display sidebar backgrounds in the Twitter UI.
252        pub profile_sidebar_fill_color: String,
253        /// The hex color chosen by the user to display text in the Twitter UI.
254        pub profile_text_color: String,
255        /// Indicates whether the user wants their uploaded background image to be used.
256        pub profile_use_background_image: bool,
257        /// Indicates whether the user is a [protected][] account.
258        ///
259        /// [protected]: https://support.twitter.com/articles/14016
260        pub protected: bool,
261        /// The screen name or handle identifying this user.
262        ///
263        /// Screen names are unique per-user but can be changed. Use `id` for an immutable identifier
264        /// for an account.
265        ///
266        /// Typically a maximum of 15 characters long, but older accounts may exist with longer screen
267        /// names.
268        pub screen_name: String,
269        /// Indicates that the user would like to see media inline. "Somewhat disused."
270        pub show_all_inline_media: Option<bool>,
271        /// If possible, the most recent tweet or retweet from this user.
272        ///
273        /// "In some circumstances, this data cannot be provided and this field will be omitted, null,
274        /// or empty." Do not depend on this field being filled. Also note that this is actually their
275        /// most-recent tweet, not the status pinned to their profile.
276        ///
277        /// "Perspectival" items within this tweet that depend on the authenticating user
278        /// [may not be completely reliable][stale-embed] in this embed.
279        ///
280        /// [stale-embed]: https://web.archive.org/web/20160617182407/https://dev.twitter.com/faq#41
281        pub status: Option<Box<tweet::Tweet>>,
282        /// The number of tweets (including retweets) posted by this user.
283        pub statuses_count: i32,
284        /// The full name of the time zone the user has set their UI preference to.
285        pub time_zone: Option<String>,
286        /// The website link given by this user in their profile.
287        pub url: Option<String>,
288        /// The UTC offset of `time_zone` in minutes.
289        pub utc_offset: Option<i32>,
290        /// Indicates whether this user is a verified account.
291        pub verified: bool,
292        /// When present, lists the countries this user has been withheld from.
293        pub withheld_in_countries: Option<Vec<String>>,
294        /// When present, indicates whether the content being withheld is a "status" or "user".
295        pub withheld_scope: Option<String>,
296    }
297}
298
299impl From<raw::RawTwitterUser> for TwitterUser {
300    fn from(mut raw: raw::RawTwitterUser) -> TwitterUser {
301        if let Some(ref description) = raw.description {
302            for entity in &mut raw.entities.description.urls {
303                codepoints_to_bytes(&mut entity.range, description);
304            }
305        }
306
307        if let (&mut Some(ref url), &mut Some(ref mut entities)) =
308            (&mut raw.url, &mut raw.entities.url)
309        {
310            for entity in &mut entities.urls {
311                codepoints_to_bytes(&mut entity.range, url);
312            }
313        }
314
315        TwitterUser {
316            contributors_enabled: raw.contributors_enabled,
317            created_at: raw.created_at,
318            default_profile: raw.default_profile,
319            default_profile_image: raw.default_profile_image,
320            description: raw.description,
321            entities: raw.entities,
322            favourites_count: raw.favourites_count,
323            follow_request_sent: raw.follow_request_sent,
324            followers_count: raw.followers_count,
325            friends_count: raw.friends_count,
326            geo_enabled: raw.geo_enabled,
327            id: raw.id,
328            is_translator: raw.is_translator,
329            lang: raw.lang,
330            listed_count: raw.listed_count,
331            location: raw.location,
332            name: raw.name,
333            profile_background_color: raw.profile_background_color,
334            profile_background_image_url: raw.profile_background_image_url,
335            profile_background_image_url_https: raw.profile_background_image_url_https,
336            profile_background_tile: raw.profile_background_tile,
337            profile_banner_url: raw.profile_banner_url,
338            profile_image_url: raw.profile_image_url,
339            profile_image_url_https: raw.profile_image_url_https,
340            profile_link_color: raw.profile_link_color,
341            profile_sidebar_border_color: raw.profile_sidebar_border_color,
342            profile_sidebar_fill_color: raw.profile_sidebar_fill_color,
343            profile_text_color: raw.profile_text_color,
344            profile_use_background_image: raw.profile_use_background_image,
345            protected: raw.protected,
346            screen_name: raw.screen_name,
347            show_all_inline_media: raw.show_all_inline_media,
348            status: raw.status,
349            statuses_count: raw.statuses_count,
350            time_zone: raw.time_zone,
351            url: raw.url,
352            utc_offset: raw.utc_offset,
353            verified: raw.verified,
354            withheld_in_countries: raw.withheld_in_countries,
355            withheld_scope: raw.withheld_scope,
356        }
357    }
358}
359
360/// Container for URL entity information that may be paired with a user's profile.
361#[derive(Debug, Clone, Default, Deserialize, Serialize)]
362pub struct UserEntities {
363    /// URL information that has been parsed out of the user's `description`. If no URLs were
364    /// detected, then the contained Vec will be empty.
365    pub description: UserEntityDetail,
366    /// Link information for the user's `url`.
367    ///
368    /// If `url` is present on the user's profile, so will this field. Twitter validates the URL
369    /// entered to a user's profile when they save it, so this can be reasonably assumed to have URL
370    /// information if it's present.
371    pub url: Option<UserEntityDetail>,
372}
373
374/// Represents a collection of URL entity information paired with a specific user profile field.
375#[derive(Debug, Clone, Default, Deserialize, Serialize)]
376pub struct UserEntityDetail {
377    /// Collection of URL entity information.
378    ///
379    /// There should be one of these per URL in the paired field. In the case of the user's
380    /// `description`, if no URLs are present, this field will still be present, but empty.
381    pub urls: Vec<entities::UrlEntity>,
382}
383
384/// Represents an active user search.
385///
386/// This struct is returned by [`search`][] and is meant to be used as a `Stream`. That means all
387/// the Stream adaptors are available:
388///
389/// [`search`]: fn.search.html
390///
391/// ```rust,no_run
392/// # use egg_mode::Token;
393/// use futures::{Stream, StreamExt, TryStreamExt};
394///
395/// # #[tokio::main]
396/// # async fn main() {
397/// # let token: Token = unimplemented!();
398/// egg_mode::user::search("rustlang", &token).take(10).try_for_each(|resp| {
399///     println!("{}", resp.screen_name);
400///     futures::future::ready(Ok(()))
401/// }).await.unwrap();
402/// # }
403/// ```
404///
405/// You can even collect the results, letting you get one set of rate-limit information for the
406/// entire search setup:
407///
408/// ```rust,no_run
409/// # use egg_mode::Token;
410/// # #[tokio::main]
411/// # async fn main() {
412/// # let token: Token = unimplemented!();
413/// use futures::{Stream, StreamExt, TryStreamExt};
414/// use egg_mode::Response;
415/// use egg_mode::user::TwitterUser;
416/// use egg_mode::error::Error;
417///
418/// // Because Streams don't have a FromIterator adaptor, we load all the responses first, then
419/// // collect them into the final Vec
420/// let names: Result<Vec<TwitterUser>, Error> =
421///     egg_mode::user::search("rustlang", &token)
422///         .take(10)
423///         .try_collect::<Vec<_>>()
424///         .await
425///         .map(|res| res.into_iter().collect());
426/// # }
427/// ```
428///
429/// `UserSearch` has a couple adaptors of its own that you can use before consuming it.
430/// `with_page_size` will let you set how many users are pulled in with a single network call, and
431/// `start_at_page` lets you start your search at a specific page. Calling either of these after
432/// starting iteration will clear any current results.
433///
434/// The `Stream` implementation yields `Response<TwitterUser>` on a successful iteration, and
435/// `Error` for errors, so network errors, rate-limit errors and other issues are passed directly
436/// through in `poll()`. The `Stream` implementation will allow you to poll again after an error to
437/// re-initiate the late network call; this way, you can wait for your network connection to return
438/// or for your rate limit to refresh and try again from the same position.
439///
440/// ## Manual paging
441///
442/// The `Stream` implementation works by loading in a page of results (with size set by default or
443/// by `with_page_size`/the `page_size` field) when it's polled, and serving the individual
444/// elements from that locally-cached page until it runs out. This can be nice, but it also means
445/// that your only warning that something involves a network call is that the stream returns
446/// `Poll::Pending`, by which time the network call has already started. If you want to know
447/// that ahead of time, that's where the `call()` method comes in. By using `call()`, you can get
448/// a page of results directly from Twitter. With that you can iterate over the results and page
449/// forward and backward as needed:
450///
451/// ```rust,no_run
452/// # use egg_mode::Token;
453/// # #[tokio::main]
454/// # async fn main() {
455/// # let token: Token = unimplemented!();
456/// let mut search = egg_mode::user::search("rustlang", &token).with_page_size(20);
457/// let resp = search.call().await.unwrap();
458///
459/// for user in resp.response {
460///    println!("{} (@{})", user.name, user.screen_name);
461/// }
462///
463/// search.page_num += 1;
464/// let resp = search.call().await.unwrap();
465///
466/// for user in resp.response {
467///    println!("{} (@{})", user.name, user.screen_name);
468/// }
469/// # }
470/// ```
471#[must_use = "search iterators are lazy and do nothing unless consumed"]
472pub struct UserSearch {
473    token: auth::Token,
474    query: CowStr,
475    /// The current page of results being returned, starting at 1.
476    pub page_num: i32,
477    /// The number of user records per page of results. Defaults to 10, maximum of 20.
478    pub page_size: i32,
479    current_loader: Option<FutureResponse<Vec<TwitterUser>>>,
480    current_results: Option<VecIter<TwitterUser>>,
481}
482
483impl UserSearch {
484    /// Sets the page size used for the search query.
485    ///
486    /// Calling this will invalidate any current search results, making the next call to `next()`
487    /// perform a network call.
488    pub fn with_page_size(self, page_size: i32) -> Self {
489        UserSearch {
490            page_size,
491            current_loader: None,
492            current_results: None,
493            ..self
494        }
495    }
496
497    /// Sets the starting page number for the search query.
498    ///
499    /// The search method begins numbering pages at 1. Calling this will invalidate any current
500    /// search results, making the next call to `next()` perform a network call.
501    pub fn start_at_page(self, page_num: i32) -> Self {
502        UserSearch {
503            page_num,
504            current_loader: None,
505            current_results: None,
506            ..self
507        }
508    }
509
510    /// Performs the search for the current page of results.
511    ///
512    /// This will automatically be called if you use the `UserSearch` as an iterator. This method is
513    /// made public for convenience if you want to manage the pagination yourself. Remember to
514    /// change `page_num` between calls.
515    pub fn call(&self) -> impl Future<Output = error::Result<Response<Vec<TwitterUser>>>> {
516        let params = ParamList::new()
517            .add_param("q", self.query.clone())
518            .add_param("page", self.page_num.to_string())
519            .add_param("count", self.page_size.to_string());
520
521        let req = get(links::users::SEARCH, &self.token, Some(&params));
522        request_with_json_response(req)
523    }
524
525    /// Returns a new UserSearch with the given query and tokens, with the default page size of 10.
526    fn new<S: Into<CowStr>>(query: S, token: &auth::Token) -> UserSearch {
527        UserSearch {
528            token: token.clone(),
529            query: query.into(),
530            page_num: 1,
531            page_size: 10,
532            current_loader: None,
533            current_results: None,
534        }
535    }
536}
537
538impl Stream for UserSearch {
539    type Item = Result<TwitterUser, error::Error>;
540
541    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
542        if let Some(mut fut) = self.current_loader.take() {
543            match Pin::new(&mut fut).poll(cx) {
544                Poll::Pending => {
545                    self.current_loader = Some(fut);
546                    return Poll::Pending;
547                }
548                Poll::Ready(Ok(res)) => self.current_results = Some(res.response.into_iter()),
549                Poll::Ready(Err(e)) => {
550                    //Invalidate current results so we don't increment the page number again
551                    self.current_results = None;
552                    return Poll::Ready(Some(Err(e)));
553                }
554            }
555        }
556
557        if let Some(ref mut results) = self.current_results {
558            if let Some(user) = results.next() {
559                return Poll::Ready(Some(Ok(user)));
560            } else if (results.len() as i32) < self.page_size {
561                return Poll::Ready(None);
562            } else {
563                self.page_num += 1;
564            }
565        }
566
567        self.current_loader = Some(Box::pin(self.call()));
568        self.poll_next(cx)
569    }
570}
571
572/// Represents relationship settings between two Twitter accounts.
573#[derive(Debug, Deserialize)]
574pub struct Relationship {
575    /// Contains settings from the perspective of the target account.
576    pub target: RelationTarget,
577    /// Contains settings from the perspective of the source account.
578    ///
579    /// This contains more information than `target` if the source account is the same as the
580    /// authenticated user. See the [`RelationSource`][] page for details.
581    ///
582    /// [`RelationSource`]: struct.RelationSource.html
583    pub source: RelationSource,
584}
585
586/// Represents relationship settings between two Twitter accounts, from the perspective of the
587/// target user.
588#[derive(Debug, Deserialize)]
589pub struct RelationTarget {
590    /// Numeric ID for this account.
591    pub id: u64,
592    /// Screen name for this account.
593    pub screen_name: String,
594    /// Indicates whether the source account follows this target account.
595    pub followed_by: bool,
596    /// Indicates whether this target account follows the source account.
597    pub following: bool,
598}
599
600/// Represents relationship settings between two Twitter accounts, from the perspective of the
601/// source user.
602///
603/// This struct holds more information than the `RelationTarget` struct, mainly attributes only
604/// visible to the user that set them. While you can see relationships between any two arbitrary
605/// users, if the "source" account is the same one whose access token you're using, you can see
606/// extra information about this relationship.
607#[derive(Debug, Deserialize)]
608pub struct RelationSource {
609    /// Numeric ID for this account.
610    pub id: u64,
611    /// Screen name for this account.
612    pub screen_name: String,
613    /// Indicates whether this source account follows the target account.
614    pub following: bool,
615    /// Indicates whether the target account follows this source account.
616    pub followed_by: bool,
617    /// Indicates whether this source account can send a direct message to the target account.
618    ///
619    /// If `followed_by` is false but this is true, that could indicate that the target account has
620    /// allowed anyone to direct-message them.
621    pub can_dm: bool,
622    /// Indicates whether this source account is blocking the target account. If the source account
623    /// is not the authenticated user, holds `None` instead.
624    pub blocking: Option<bool>,
625    /// Indicates whether this source account has reported the target account for spam. If the source
626    /// account is not the authenticated user, holds `None` instead.
627    pub marked_spam: Option<bool>,
628    /// Indicates whether this source account has decided to receive all replies from the target
629    /// account. If the source account is not the authenticated user, holds `None` instead.
630    ///
631    /// Note that there is no mechanism with which to toggle this setting, at least none that this
632    /// author could find, either through the API or through the official site.
633    all_replies: Option<bool>,
634    /// Indicates whether this source account has decided to show retweets from the target account.
635    /// If the source account is not the authenticated user, holds `None` instead.
636    pub want_retweets: Option<bool>,
637    /// Indicates whether this source account has decided to receive mobile notifications for the
638    /// target account. If the source account is not the authenticated user, holds `None` instead.
639    pub notifications_enabled: Option<bool>,
640}
641
642/// Represents the relation the authenticated user has to a given account.
643///
644/// This is returned by `relation_lookup`, as opposed to `Relationship`, which is returned by
645/// `relation`.
646#[derive(Debug, Deserialize)]
647pub struct RelationLookup {
648    /// The display name of the target account.
649    pub name: String,
650    /// The screen name of the target account.
651    pub screen_name: String,
652    /// The numeric ID of the target account.
653    pub id: u64,
654    /// The ways the target account is connected to the authenticated user.
655    ///
656    /// If the target account has no relation to the authenticated user, this will not be empty; its
657    /// only element will be `None`.
658    pub connections: Vec<Connection>,
659}
660
661/// Represents the ways a target account can be connected to another account.
662#[derive(Debug, Deserialize)]
663pub enum Connection {
664    /// The target account has no relation.
665    #[serde(rename = "none")]
666    None,
667    /// The authenticated user has requested to follow the target account.
668    #[serde(rename = "following_requested")]
669    FollowingRequested,
670    /// The target account has requested to follow the authenticated user.
671    #[serde(rename = "following_received")]
672    FollowingReceived,
673    /// The target account follows the authenticated user.
674    #[serde(rename = "followed_by")]
675    FollowedBy,
676    /// The authenticated user follows the target account.
677    #[serde(rename = "following")]
678    Following,
679    /// The authenticated user has blocked the target account.
680    #[serde(rename = "blocking")]
681    Blocking,
682    /// The authenticated user has muted the target account.
683    #[serde(rename = "muting")]
684    Muting,
685}
686
687#[cfg(test)]
688mod tests {
689    use super::TwitterUser;
690    use crate::common::tests::load_file;
691
692    #[test]
693    fn roundtrip_deser() {
694        let sample = load_file("sample_payloads/user_array.json");
695        let users_src: Vec<TwitterUser> = serde_json::from_str(&sample).unwrap();
696        let json1 = serde_json::to_value(users_src).unwrap();
697        let users_roundtrip: Vec<TwitterUser> = serde_json::from_value(json1.clone()).unwrap();
698        let json2 = serde_json::to_value(users_roundtrip).unwrap();
699
700        assert_eq!(json1, json2);
701    }
702}