roblox_api/api/friends/
v1.rs

1use reqwest::Method;
2use serde::{Deserialize, Serialize, de::DeserializeOwned};
3
4use crate::{DateTime, Error, Paging, client::Client};
5
6pub const URL: &str = "https://friends.roblox.com/v1";
7
8#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
9#[serde(rename_all = "camelCase")]
10pub struct FollowingStatus {
11    #[serde(rename = "userId")]
12    pub id: u64,
13    pub is_following: bool,
14    pub is_followed: bool,
15}
16
17#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
18pub struct FriendStatus {
19    pub id: u64,
20    pub status: String,
21}
22
23#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
24pub enum FriendRequestSourceType {
25    InGame,
26    UserProfile,
27    PlayerSearch,
28    FriendRecommendations,
29}
30
31#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
32#[serde(rename_all = "camelCase")]
33pub struct FriendRequester {
34    #[serde(rename = "senderId")]
35    pub id: u64,
36    #[serde(rename = "senderNickname")]
37    pub display_name: String,
38    pub contact_name: Option<String>,
39
40    pub source_universe_id: u64,
41    pub origin_source_type: FriendRequestSourceType,
42    pub sent_at: DateTime,
43}
44
45#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
46#[serde(rename_all = "camelCase")]
47pub struct FriendRequest {
48    pub id: u64,
49    pub mutual_friends_list: Vec<String>,
50    #[serde(rename = "friendRequest")]
51    pub requester: FriendRequester,
52}
53
54#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
55#[serde(rename_all = "camelCase")]
56pub struct FriendRequests {
57    #[serde(rename = "data")]
58    pub requests: Vec<FriendRequest>,
59    #[serde(rename = "nextPageCursor")]
60    pub next_cursor: Option<String>,
61    #[serde(rename = "previousPageCursor")]
62    pub previous_cursor: Option<String>,
63}
64
65#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
66#[serde(rename_all = "camelCase")]
67pub struct User {
68    pub id: u64,
69    #[serde(rename = "hasVerifiedBadge")]
70    pub is_verified: Option<bool>,
71}
72
73#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
74#[serde(rename_all = "camelCase")]
75pub struct Followers {
76    #[serde(rename = "data")]
77    pub users: Vec<User>,
78    #[serde(rename = "nextPageCursor")]
79    pub next_cursor: Option<String>,
80    #[serde(rename = "previousPageCursor")]
81    pub previous_cursor: Option<String>,
82}
83
84#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
85#[serde(rename_all = "PascalCase")]
86pub struct FriendsFind {
87    #[serde(rename = "PageItems")]
88    pub users: Vec<User>,
89    pub next_cursor: Option<String>,
90    pub previous_cursor: Option<String>,
91    pub has_more: Option<bool>,
92}
93
94#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
95#[serde(rename_all = "camelCase")]
96pub struct UserPresence {
97    #[serde(rename = "UserPresenceType")]
98    pub kind: String,
99    #[serde(rename = "UserLocationType")]
100    pub location_kind: String,
101
102    #[serde(rename = "lastLocation")]
103    pub status: String,
104    pub last_online: DateTime,
105
106    pub place_id: Option<u64>,
107    pub root_place_id: Option<u64>,
108    pub universe_id: Option<u64>,
109    #[serde(rename = "gameInstanceId")]
110    pub job_id: Option<String>,
111}
112
113#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
114#[serde(rename_all = "camelCase")]
115pub struct FriendOnlineStatus {
116    pub id: u64,
117    #[serde(rename = "userPresence")]
118    pub presence: UserPresence,
119}
120
121async fn generic_request<'a, R: Serialize, T: DeserializeOwned>(
122    client: &mut Client,
123    method: Method,
124    path: &str,
125    request: Option<&'a R>,
126    query: Option<&'a [(&'a str, &'a str)]>,
127) -> Result<T, Error> {
128    let mut builder = client
129        .requestor
130        .client
131        .request(method, format!("{URL}/{path}"))
132        .headers(client.requestor.default_headers.clone());
133
134    // Even though sending None works, it might get serialized as null in json, which is a waste of bytes
135    if let Some(request) = request {
136        builder = builder.json(&request);
137    }
138
139    if let Some(query) = query {
140        builder = builder.query(&query);
141    }
142
143    let response = client.validate_response(builder.send().await).await?;
144    client.requestor.parse_json::<T>(response).await
145}
146
147async fn generic_count(client: &mut Client, path: &str) -> Result<u16, Error> {
148    #[derive(Debug, Deserialize)]
149    struct Response {
150        count: u16,
151    }
152
153    Ok(
154        generic_request::<(), Response>(client, Method::GET, &format!("{path}/count"), None, None)
155            .await?
156            .count,
157    )
158}
159
160pub async fn friend_requests_count(client: &mut Client) -> Result<u16, Error> {
161    generic_count(client, "user/friend-requests").await
162}
163
164pub async fn new_friend_requests_count(client: &mut Client) -> Result<u16, Error> {
165    generic_count(client, "my/new-friend-requests").await
166}
167
168pub async fn user_friends_count(client: &mut Client, id: u64) -> Result<u16, Error> {
169    generic_count(client, &format!("users/{id}/friends")).await
170}
171
172pub async fn user_followings_count(client: &mut Client, id: u64) -> Result<u16, Error> {
173    generic_count(client, &format!("users/{id}/followings")).await
174}
175
176pub async fn user_followers_count(client: &mut Client, id: u64) -> Result<u16, Error> {
177    generic_count(client, &format!("users/{id}/followers")).await
178}
179
180pub async fn following_status(
181    client: &mut Client,
182    ids: &[u64],
183) -> Result<Vec<FollowingStatus>, Error> {
184    #[derive(Debug, Serialize)]
185    struct Request<'a> {
186        #[serde(rename = "targetUserIds")]
187        user_ids: &'a [u64],
188    }
189
190    #[derive(Debug, Deserialize)]
191    struct Response {
192        #[serde(rename = "followings")]
193        statuses: Vec<FollowingStatus>,
194    }
195
196    Ok(generic_request::<Request, Response>(
197        client,
198        Method::POST,
199        "user/following-exists",
200        Some(&Request { user_ids: ids }),
201        None,
202    )
203    .await?
204    .statuses)
205}
206
207pub async fn friend_requests(
208    client: &mut Client,
209    paging: Paging<'_>,
210) -> Result<FriendRequests, Error> {
211    let limit = paging.limit.unwrap_or(18).to_string();
212    let cursor = paging.cursor.unwrap_or("");
213
214    generic_request::<(), FriendRequests>(
215        client,
216        Method::GET,
217        "my/friends/requests",
218        None,
219        Some(&[("cursor", cursor), ("limit", &limit)]),
220    )
221    .await
222}
223
224pub async fn user_followers(client: &mut Client, id: u64) -> Result<Followers, Error> {
225    generic_request::<(), Followers>(
226        client,
227        Method::GET,
228        &format!("users/{id}/followers"),
229        None,
230        None,
231    )
232    .await
233}
234
235pub async fn user_followings(client: &mut Client, id: u64) -> Result<Followers, Error> {
236    generic_request::<(), Followers>(
237        client,
238        Method::GET,
239        &format!("users/{id}/followings"),
240        None,
241        None,
242    )
243    .await
244}
245
246pub async fn user_friends_online(
247    client: &mut Client,
248    id: u64,
249) -> Result<Vec<FriendOnlineStatus>, Error> {
250    #[derive(Debug, Deserialize)]
251    struct Response {
252        #[serde(rename = "data")]
253        online: Vec<FriendOnlineStatus>,
254    }
255
256    Ok(generic_request::<(), Response>(
257        client,
258        Method::GET,
259        &format!("users/{id}/friends/online"),
260        None,
261        None,
262    )
263    .await?
264    .online)
265}
266
267pub async fn user_friends_find(
268    client: &mut Client,
269    id: u64,
270    paging: Paging<'_>,
271) -> Result<FriendsFind, Error> {
272    let limit = paging.limit.unwrap_or(18).to_string();
273    let cursor = paging.cursor.unwrap_or("");
274
275    generic_request::<(), FriendsFind>(
276        client,
277        Method::GET,
278        &format!("users/{id}/friends/find"),
279        None,
280        Some(&[("cursor", cursor), ("limit", &limit), ("userSort", "1")]),
281    )
282    .await
283}
284
285pub async fn user_friends_search(
286    client: &mut Client,
287    id: u64,
288    query: &str,
289    paging: Paging<'_>,
290) -> Result<FriendsFind, Error> {
291    let limit = paging.limit.unwrap_or(36).to_string();
292    let cursor = paging.cursor.unwrap_or("");
293
294    generic_request::<(), FriendsFind>(
295        client,
296        Method::GET,
297        &format!("users/{id}/friends/search"),
298        None,
299        Some(&[("cursor", &cursor), ("limit", &limit), ("query", query)]),
300    )
301    .await
302}
303
304pub async fn user_friend_statuses(
305    client: &mut Client,
306    id: u64,
307    friends: &[u64],
308) -> Result<Vec<FriendStatus>, Error> {
309    #[derive(Debug, Deserialize)]
310    struct Response {
311        #[serde(rename = "data")]
312        statuses: Vec<FriendStatus>,
313    }
314
315    let ids = friends
316        .iter()
317        .map(|x| x.to_string())
318        .collect::<Vec<String>>()
319        .join(",");
320
321    Ok(generic_request::<(), Response>(
322        client,
323        Method::GET,
324        &format!("users/{id}/friends/statuses"),
325        None,
326        Some(&[("userIds", &ids)]),
327    )
328    .await?
329    .statuses)
330}