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 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}