egg_mode/list/
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 functions for working with lists.
6//!
7//! A list is a way to group accounts together, either as a way to highlight those accounts, or to
8//! view as a subset of or supplement to your main timeline. A list can be viewed in the same way
9//! as the user's own timelines, loading in pages with the same [`Timeline`] interface.
10//!
11//! [`Timeline`]: ../tweet/struct.Timeline.html
12//!
13//! Lists can be private or public. If a list if public, then other users can view the list's
14//! members and the statuses of those members. (Statuses by protected accounts can still only be
15//! read by approved followers.)
16//!
17//! A list has "members", those accounts that are tracked as part of the list. When you call
18//! `statuses` on a given list, you're looking at the recent posts by its members. Members can be
19//! added and removed either one at a time, or in batches. Protected accounts can only be added to
20//! a list if the user is an approved follower of that account.
21//!
22//! A user can "subscribe" to another user's list as a sort of bookmark. Doing this places the list
23//! in the "lists" section of their profile, and in the `subscriptions` and `list` sets available
24//! from the API. Note that you don't need to subscribe to a list to view the statuses or users in
25//! it; you can do that to any public or personally owned list anyway. Subscribing merely allows
26//! quick access in case you want to keep track of a list someone else made.
27//!
28//! If a list is public, then all metadata about that list is public. Protected accounts can be
29//! seen as members of a list, but their statuses are still only visible to approved followers.
30//! None of the user-focused queries in this module assume that they're about the authenticated
31//! user; they can all be called in reference to any user.
32//!
33//! ## Types
34//!
35//! - `List`: This is the list metadata returned from Twitter when requesting information about the
36//!   list itself, or when performing some modification to one.
37//! - `ListID`: There are two ways to reference a list in the Twitter API: Either via a unique
38//!   numeric ID, or with its "slug" combined with a reference to the user who created it. This
39//!   enum wraps that distinction into one type that all the methods take when they need to
40//!   reference a list like this. See the enum's documentation for details on how to create one.
41//! - `ListUpdate`: When updating a list's metadata, all the fields that can be updated are
42//!   optional, so the `update` function returns this builder struct so you don't have to provide
43//!   all the parameters if you don't need to.
44//!
45//! ## Functions
46//!
47//! ### Basic actions
48//!
49//! These functions perform basic write actions on lists as a whole. These all require write access
50//! to the authenticated user's account.
51//!
52//! - `create`/`delete`
53//! - `update` (see `ListUpdate` for full details)
54//! - `subscribe`/`unsubscribe`
55//! - `add_member`/`remove_member`
56//! - `add_member_list`/`remove_member_list`
57//!
58//! ### Basic queries
59//!
60//! These functions let you query information about lists, or related to lists somehow.
61//!
62//! - `ownerships`/`subscriptions`/`list`: Note that `list` will only return the most recent 100
63//!   lists in the `ownerships`/`subscriptions` sets.
64//! - `memberships`
65//! - `members`/`is_member`
66//! - `subscribers`/`is_subscriber`
67//! - `show`
68//! - `statuses`
69
70use chrono;
71use serde::Deserialize;
72
73use crate::common::*;
74use crate::{auth, links, user};
75
76mod fun;
77pub use self::fun::*;
78
79/// Convenience enum to refer to a list via its owner and name or via numeric ID.
80///
81/// Any API call that needs to reference a specific list has a set of parameters that collectively
82/// refer to it. Not only do lists have a unique numeric ID that refers to them, they have a "slug"
83/// that stands in as the list's unique name. This slug is only unique when taken in combination
84/// with the user that created it, though, so this leads to the raw API call having parameters that
85/// refer to the user by screen name or ID, or the list as a whole by this pair of slug parameters
86/// or the single ID parameter. egg-mode wraps this pattern with this `ListID` enum.
87///
88/// Because the slug is formed from two variables instead of one, this enum foregoes the many
89/// `From` implementations that `UserID` has and instead opts for two creation functions. If you
90/// have a user/name combo, use `ListID::from_slug` when looking for the list. If you have the
91/// list's ID instead, then you can use `ListID::from_id`.
92///
93/// # Example
94///
95/// ```rust
96/// use egg_mode::list::ListID;
97///
98/// //The following two ListIDs refer to the same list:
99/// let slug = ListID::from_slug("Twitter", "support");
100/// let id = ListID::from_id(99924643);
101/// ```
102#[derive(Debug, Clone)]
103pub enum ListID {
104    ///Referring via the list's owner and its "slug" or name.
105    Slug(user::UserID, CowStr),
106    ///Referring via the list's numeric ID.
107    ID(u64),
108}
109
110impl ListID {
111    ///Make a new `ListID` by supplying its owner and name.
112    pub fn from_slug<T: Into<user::UserID>>(owner: T, list_name: impl Into<CowStr>) -> ListID {
113        ListID::Slug(owner.into(), list_name.into())
114    }
115
116    ///Make a new `ListID` by supplying its numeric ID.
117    pub fn from_id(list_id: u64) -> ListID {
118        ListID::ID(list_id)
119    }
120}
121
122/// Represents the metadata for a list.
123///
124/// Because of the myriad ways to reference a list, there are a few seemingly-redundant fields on
125/// here. It's worthwhile to understand all the referential fields:
126///
127/// * `name` is the human-readable name of the list. Notably, this can contain spaces and uppercase
128///   letters.
129/// * `slug` is simply `name` converted to a format that can be put into a URL and used to
130///   reference the list for API calls.
131/// * `full_name` is how you'd link the list as a @mention, in the form `@screen_name/slug`.
132/// * `id` is the numeric ID, which can be used with `ListID::from_id` to make a `ListID` for the
133///   list.
134/// * `uri` is how you assemble a link to the list. Start with `"https://twitter.com"`, concat this
135///   field to the end, and you have a full URL. Note that the field does start with its own slash.
136/// * `user` is a mostly-populated `TwitterUser` corresponding to the creator of the list. If you
137///   combine `user.screen_name` or `user.id` with `slug`, you can send them to `ListID::from_slug`
138///   to make a `ListID` for the list.
139#[derive(Clone, Debug, Deserialize)]
140pub struct List {
141    ///The name of the list.
142    pub name: String,
143    ///The user who created the list.
144    pub user: user::TwitterUser,
145    ///The "slug" of a list, that can be combined with its creator's `UserID` to refer to the list.
146    pub slug: String,
147    ///The numeric ID of the list.
148    pub id: u64,
149    ///The number of accounts "subscribed" to the list, for whom it will appear in their collection
150    ///of available lists.
151    pub subscriber_count: u64,
152    ///The number of accounts added to the list.
153    pub member_count: u64,
154    ///The full name of the list, preceded by `@`, that can be used to link to the list as part of
155    ///a tweet, direct message, or other place on Twitter where @mentions are parsed.
156    pub full_name: String,
157    ///The description of the list, as entered by its creator.
158    pub description: String,
159    ///The full name of the list, preceded by `/`, that can be preceded with `https://twitter.com`
160    ///to create a link to the list.
161    pub uri: String,
162    ///UTC timestamp of when the list was created.
163    #[serde(with = "serde_datetime")]
164    pub created_at: chrono::DateTime<chrono::Utc>,
165}
166
167/// Represents a pending update to a list's metadata.
168///
169/// As updating a list could modify each field independently, this operation is exposed as a builder
170/// struct. To update any field, call the method named after that field, then call `send` to send
171/// the update to Twitter.
172///
173/// # Example
174///
175/// ```rust,no_run
176/// # use egg_mode::Token;
177/// # #[tokio::main]
178/// # async fn main() {
179/// # let token: Token = unimplemented!();
180/// use egg_mode::list::{self, ListID};
181///
182/// //remember, you can only update a list if you own it!
183/// let update = list::update(ListID::from_slug("Twitter", "support"));
184/// let list = update.name("Official Support").send(&token).await.unwrap();
185/// # }
186/// ```
187pub struct ListUpdate {
188    list: ListID,
189    name: Option<String>,
190    public: Option<bool>,
191    desc: Option<String>,
192}
193
194impl ListUpdate {
195    ///Updates the name of the list.
196    pub fn name(self, name: impl Into<String>) -> ListUpdate {
197        ListUpdate {
198            name: Some(name.into()),
199            ..self
200        }
201    }
202
203    ///Sets whether the list is public.
204    pub fn public(self, public: bool) -> ListUpdate {
205        ListUpdate {
206            public: Some(public),
207            ..self
208        }
209    }
210
211    ///Updates the description of the list.
212    pub fn desc(self, desc: String) -> ListUpdate {
213        ListUpdate {
214            desc: Some(desc),
215            ..self
216        }
217    }
218
219    ///Sends the update request to Twitter.
220    pub async fn send(self, token: &auth::Token) -> Result<Response<List>, crate::error::Error> {
221        let params = ParamList::new()
222            .add_list_param(self.list)
223            .add_opt_param("name", self.name)
224            .add_opt_param(
225                "mode",
226                self.public.map(|p| if p { "public" } else { "private" }),
227            )
228            .add_opt_param("description", self.desc);
229
230        let req = post(links::lists::UPDATE, token, Some(&params));
231        request_with_json_response(req).await
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::List;
238    use crate::common::tests::load_file;
239
240    #[test]
241    fn parse_list_sample() {
242        let content = load_file("sample_payloads/sample-list.json");
243        let list = ::serde_json::from_str::<List>(&content).unwrap();
244        assert_eq!(list.full_name, "@Scobleizer/all-people-in-spatial-2");
245        assert_eq!(list.user.screen_name, "Scobleizer")
246    }
247}