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(¶ms));
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}