rustyroblox/groups/
groups.rs

1use rspc::Type;
2use serde::{Deserialize, Serialize};
3use strum_macros::{Display, EnumString};
4
5use crate::{
6    users::{whoami, MinimalGroupUser},
7    util::{
8        jar::RequestJar,
9        paging::{get_page, PageLimit, SortOrder},
10        responses::DataWrapper,
11        Error,
12    },
13};
14
15use super::{permissions::GroupPermissions, roles::GroupRole};
16
17#[derive(Debug, Serialize, Deserialize, Clone, Type)]
18#[serde(rename_all = "camelCase")]
19pub struct Group {
20    pub id: i64,
21    pub name: String,
22    pub description: String,
23    pub owner: MinimalGroupUser,
24    pub shout: Option<GroupShout>,
25    pub member_count: Option<i64>,
26    pub is_builders_club_only: bool,
27    pub public_entry_allowed: bool,
28    pub is_locked: Option<bool>,
29    pub has_verified_badge: bool,
30}
31
32#[derive(Debug, Serialize, Deserialize, Clone, Type)]
33#[serde(rename_all = "camelCase")]
34pub struct MinimalGroup {
35    pub id: i64,
36    pub name: String,
37    pub member_count: i64,
38    pub has_verified_badge: bool,
39}
40
41#[derive(Debug, Serialize, Deserialize, Clone, Type)]
42#[serde(rename_all = "camelCase")]
43pub struct GroupShout {
44    pub body: String,
45    pub poster: MinimalGroupUser,
46    pub created: String,
47    pub updated: String,
48}
49
50/// Gets a group by its group ID
51///
52/// # Error codes
53/// - 1: Group is invalid or does not exist.
54pub async fn group_by_id(jar: &RequestJar, group_id: i64) -> Result<Group, Box<Error>> {
55    let url = format!("https://groups.roblox.com/v1/groups/{}", group_id);
56    let response = jar.get_json::<Group>(&url).await?;
57    Ok(response)
58}
59
60#[derive(Debug, Serialize, Deserialize, Clone, Type)]
61#[serde(rename_all = "camelCase")]
62pub struct GroupAuditLogEntry {
63    pub actor: GroupAuditLogActor,
64    pub action_type: GroupAuditLogActionType,
65    //description: , // FIXME: ??? It shows an empty object in the docs
66    pub created: String,
67}
68
69#[derive(Debug, Serialize, Deserialize, Clone, Type)]
70#[serde(rename_all = "camelCase")]
71pub struct GroupAuditLogActor {
72    pub user: MinimalGroupUser,
73    pub created: String,
74    pub updated: String,
75}
76
77#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Display, EnumString, Type)]
78pub enum GroupAuditLogActionType {
79    // This is imported from the docs's HTML
80    DeletePost,
81    RemoveMember,
82    AcceptJoinRequest,
83    DeclineJoinRequest,
84    PostStatus,
85    ChangeRank,
86    BuyAd,
87    SendAllyRequest,
88    CreateEnemy,
89    AcceptAllyRequest,
90    DeclineAllyRequest,
91    DeleteAlly,
92    DeleteEnemy,
93    AddGroupPlace,
94    RemoveGroupPlace,
95    CreateItems,
96    ConfigureItems,
97    SpendGroupFunds,
98    ChangeOwner,
99    Delete,
100    AdjustCurrencyAmounts,
101    Abandon,
102    Claim,
103    Rename,
104    ChangeDescription,
105    InviteToClan,
106    KickFromClan,
107    CancelClanInvite,
108    BuyClan,
109    CreateGroupAsset,
110    UpdateGroupAsset,
111    ConfigureGroupAsset,
112    RevertGroupAsset,
113    CreateGroupDeveloperProduct,
114    ConfigureGroupGame,
115    Lock,
116    Unlock,
117    CreateGamePass,
118    CreateBadge,
119    ConfigureBadge,
120    SavePlace,
121    PublishPlace,
122    UpdateRolesetRank,
123    UpdateRolesetData,
124}
125
126/// Gets the audit log for a group
127///
128/// # Error codes
129/// - 1: Group is invalid or does not exist.
130/// - 23: Insufficient permissions to complete the request.
131pub async fn audit_log(
132    jar: &RequestJar,
133    group_id: i64,
134    limit: PageLimit,
135    user_id: Option<i64>,
136    sort_order: Option<SortOrder>,
137    //cursor: Option<String>,
138) -> Result<Vec<GroupAuditLogEntry>, Box<Error>> {
139    let mut url = format!("https://groups.roblox.com/v1/groups/{}/audit-log", group_id);
140    if user_id.is_some() {
141        let user_id = user_id.unwrap();
142        url = format!("{}?userId={}", url, user_id);
143    }
144    url = format!("{}&sortOrder={}", url, sort_order.unwrap_or(SortOrder::Asc));
145
146    let response = get_page(jar, url.as_str(), limit, None).await?; // TODO: cursor
147    Ok(response)
148}
149
150#[derive(Debug, Serialize, Deserialize, Clone, Type)]
151#[serde(rename_all = "camelCase")]
152pub struct GroupNameHistoryEntry {
153    pub name: String,
154    pub created: String,
155}
156
157/// Gets the name history for a group
158///
159/// # Error codes
160/// - 1: Group is invalid or does not exist.
161/// - 23: Insufficient permissions to complete the request.
162pub async fn name_history(
163    jar: &RequestJar,
164    group_id: i64,
165    limit: PageLimit,
166    sort_order: Option<SortOrder>,
167    //cursor: Option<String>,
168) -> Result<Vec<GroupNameHistoryEntry>, Box<Error>> {
169    let mut url = format!(
170        "https://groups.roblox.com/v1/groups/{}/name-history",
171        group_id
172    );
173    url = format!("{}?sortOrder={}", url, sort_order.unwrap_or(SortOrder::Asc));
174
175    let response = get_page(jar, url.as_str(), limit, None).await?; // TODO: cursor
176    Ok(response)
177}
178
179#[derive(Debug, Serialize, Deserialize, Clone, Type)]
180#[serde(rename_all = "camelCase")]
181pub struct GroupSettings {
182    pub is_approval_required: bool,
183    pub is_builders_club_required: bool,
184    pub are_enemies_allowed: bool,
185    pub are_group_funds_visible: bool,
186    pub are_group_games_visible: bool,
187    pub is_group_name_change_enabled: bool,
188}
189
190/// Gets a group's settings
191///
192/// # Error codes
193/// - 1: Group is invalid or does not exist.
194/// - 23: Insufficient permissions to complete the request.
195pub async fn settings(jar: &RequestJar, group_id: i64) -> Result<GroupSettings, Box<Error>> {
196    let url = format!("https://groups.roblox.com/v1/groups/{}/settings", group_id);
197    let response = jar.get_json::<GroupSettings>(&url).await?;
198    Ok(response)
199}
200
201#[derive(Debug, Serialize, Deserialize, Clone, Type)]
202#[serde(rename_all = "camelCase")]
203pub struct GroupSettingsUpdateRequest {
204    pub is_approval_required: Option<bool>,
205    pub are_enemies_allowed: Option<bool>,
206    pub are_group_funds_visible: Option<bool>,
207    pub are_group_games_visible: Option<bool>,
208}
209
210#[derive(Debug, Serialize, Deserialize, Clone, Type)]
211#[serde(rename_all = "camelCase")]
212pub struct GroupSettingsUpdateResponse {}
213
214/// Updates a group's settings
215///
216/// # Error codes
217/// - 1: Group is invalid or does not exist.
218/// - 23: Insufficient permissions to complete the request.
219/// - 31: Service is currently unavailable.
220pub async fn update_settings(
221    jar: &RequestJar,
222    group_id: i64,
223    request: GroupSettingsUpdateRequest,
224) -> Result<GroupSettingsUpdateResponse, Box<Error>> {
225    let url = format!("https://groups.roblox.com/v1/groups/{}/settings", group_id);
226    let response = jar
227        .patch_json::<GroupSettingsUpdateResponse, GroupSettingsUpdateRequest>(&url, request)
228        .await?;
229    Ok(response)
230}
231
232// TODO: Figure out how to send the files to /v1/groups/create and implement it
233
234#[derive(Debug, Serialize, Deserialize, Clone, Type)]
235#[serde(rename_all = "camelCase")]
236pub struct GroupComplianceItem {
237    pub can_view_group: bool,
238    pub group_id: i64,
239}
240
241#[derive(Debug, Serialize, Deserialize, Clone, Type)]
242#[serde(rename_all = "camelCase")]
243pub struct GroupComplianceResponse {
244    pub groups: Vec<GroupComplianceItem>,
245}
246
247#[derive(Debug, Serialize, Deserialize, Clone, Type)]
248#[serde(rename_all = "camelCase")]
249pub struct GroupComplianceRequest {
250    pub group_ids: Vec<i64>,
251}
252
253/// Gets group policy info used for compliance
254/// # Error codes
255/// - 1: Too many ids in request.
256/// - 2: Ids could not be parsed from request.
257pub async fn compliance(
258    jar: &RequestJar,
259    group_ids: Vec<i64>,
260) -> Result<GroupComplianceResponse, Box<Error>> {
261    let url = format!("https://groups.roblox.com/v1/groups/policies");
262    let request = GroupComplianceRequest { group_ids };
263    let response = jar
264        .post_json::<GroupComplianceResponse, GroupComplianceRequest>(&url, request)
265        .await?;
266    Ok(response)
267}
268
269#[derive(Debug, Serialize, Deserialize, Clone, Type)]
270#[serde(rename_all = "camelCase")]
271pub struct NewDescriptionRequest {
272    pub description: String,
273}
274
275#[derive(Debug, Serialize, Deserialize, Clone, Type)]
276#[serde(rename_all = "camelCase")]
277pub struct NewDescriptionResponse {
278    pub new_description: String,
279}
280
281/// Updates a group's description
282/// # Error codes
283/// - 1: Group is invalid or does not exist.
284/// - 18: The description is too long.
285/// - 23: Insufficient permissions to complete the request.
286/// - 29: Your group description was empty.
287pub async fn update_description(
288    jar: &RequestJar,
289    group_id: i64,
290    description: String,
291) -> Result<NewDescriptionResponse, Box<Error>> {
292    let url = format!(
293        "https://groups.roblox.com/v1/groups/{}/description",
294        group_id
295    );
296    let request = NewDescriptionRequest { description };
297    let response = jar
298        .patch_json::<NewDescriptionResponse, NewDescriptionRequest>(&url, request)
299        .await?;
300    Ok(response)
301}
302
303#[derive(Debug, Serialize, Deserialize, Clone, Type)]
304#[serde(rename_all = "camelCase")]
305pub struct NewNameRequest {
306    pub name: String,
307}
308
309#[derive(Debug, Serialize, Deserialize, Clone, Type)]
310#[serde(rename_all = "camelCase")]
311pub struct NewNameResponse {
312    pub new_name: String,
313}
314
315/// Updates a group's name
316///
317/// **THIS COSTS ROBUX!**
318///
319/// # Error codes
320/// - 1: Group is invalid or does not exist.
321/// - 18: The description is too long.
322/// - 23: Insufficient permissions to complete the request.
323/// - 29: Your group description was empty.
324pub async fn update_name(
325    jar: &RequestJar,
326    group_id: i64,
327    name: String,
328) -> Result<NewNameResponse, Box<Error>> {
329    let url = format!(
330        "https://groups.roblox.com/v1/groups/{}/description",
331        group_id
332    );
333    let request = NewNameRequest { name };
334    let response = jar
335        .patch_json::<NewNameResponse, NewNameRequest>(&url, request)
336        .await?;
337    Ok(response)
338}
339
340// FIXME: There is an endpoint PATCH /v1/groups/{groupId}/status, is it needed? what does it do? pls research
341
342// TODO: Implement /v1/groups/icon, i have no idea how to upload files
343
344#[derive(Debug, Serialize, Deserialize, Clone, Type)]
345#[serde(rename_all = "camelCase")]
346pub struct GroupMembership {
347    pub group_id: i64,
348    pub is_primary: bool,
349    pub is_pending_join: bool,
350    pub group_role: Option<GroupMembershipUserRole>,
351    pub permissions: GroupPermissions,
352    pub are_group_games_visible: bool,
353    pub are_group_funds_visible: bool,
354    pub are_enemies_allowed: bool,
355    pub can_configure: bool,
356}
357
358#[derive(Debug, Serialize, Deserialize, Clone, Type)]
359#[serde(rename_all = "camelCase")]
360pub struct GroupMembershipUserRole {
361    pub user: MinimalGroupUser,
362    pub role: GroupRole,
363}
364
365/// Gets a user's group membership
366///
367/// # Error codes
368/// - 1: Group is invalid or does not exist.
369pub async fn membership(jar: &RequestJar, group_id: i64) -> Result<GroupMembership, Box<Error>> {
370    let url = format!(
371        "https://groups.roblox.com/v1/groups/{}/membership",
372        group_id
373    );
374    let response = jar.get_json::<GroupMembership>(&url).await?;
375    Ok(response)
376}
377
378/// Gets a list of users in a group
379///
380/// # Error codes
381/// - 1: The group is invalid or does not exist.
382pub async fn members(
383    jar: &RequestJar,
384    group_id: i64,
385    limit: PageLimit,
386    sort_order: Option<SortOrder>,
387) -> Result<Vec<GroupMembershipUserRole>, Box<Error>> {
388    let url = format!(
389        "https://groups.roblox.com/v1/groups/{}/users?sortOrder={}",
390        group_id,
391        sort_order.unwrap_or(SortOrder::Asc).get_sort_order_string()
392    );
393    //let response = jar.get_json::<GroupRoleResponse>(&url).await?;
394    let response = get_page(jar, url.as_str(), limit, None).await?;
395    Ok(response)
396}
397
398// Note: Joining a group is not implemented and will not be implemented, as it is not needed and requires a captcha.
399
400/// Gets all groups the authenticated user is pending for
401/// **This does not list the pend requests for a specific group**
402///
403/// # Error codes
404/// There are no error codes for this endpoint.
405pub async fn pending_requests(jar: &RequestJar) -> Result<Vec<Group>, Box<Error>> {
406    let url = format!("https://groups.roblox.com/v1/user/groups/pending");
407    let response = jar
408        .get_json::<DataWrapper<Vec<Group>>>(url.as_str())
409        .await?;
410    Ok(response.data)
411}
412
413#[derive(Debug, Serialize, Deserialize, Clone, Type)]
414#[serde(rename_all = "camelCase")]
415pub struct FriendGroupsGroupItem {
416    pub group: Group,
417    pub role: GroupRole,
418    pub is_primary_group: Option<bool>,
419}
420
421#[derive(Debug, Serialize, Deserialize, Clone, Type)]
422#[serde(rename_all = "camelCase")]
423pub struct FriendGroupsItem {
424    pub user: MinimalGroupUser,
425    pub groups: Vec<FriendGroupsGroupItem>,
426}
427
428/// Gets all the groups the currently authenticated user's friends are in
429///
430/// # Error codes
431/// - 3: The user is invalid or does not exist.
432pub async fn friend_groups(jar: &RequestJar) -> Result<Vec<FriendGroupsItem>, Box<Error>> {
433    let user_id = whoami(jar).await?.id;
434    let url = format!(
435        "https://groups.roblox.com/v1/users/{}/friends/groups/roles",
436        user_id
437    );
438    let response = jar
439        .get_json::<DataWrapper<Vec<FriendGroupsItem>>>(url.as_str())
440        .await?;
441    Ok(response.data)
442}
443
444#[derive(Debug, Serialize, Deserialize, Clone, Type)]
445#[serde(rename_all = "camelCase")]
446pub struct UserMembershipsGroupItem {
447    pub group: MinimalGroup,
448    pub role: GroupRole,
449}
450
451/// Gets all the groups the specified user is in.
452/// It also includes the role the user is
453///
454/// # Error codes
455/// - 3: The user is invalid or does not exist.
456pub async fn user_memberships(
457    jar: &RequestJar,
458    user_id: i64,
459) -> Result<Vec<UserMembershipsGroupItem>, Box<Error>> {
460    let url = format!(
461        "https://groups.roblox.com/v2/users/{}/groups/roles",
462        user_id
463    );
464    let response = jar
465        .get_json::<DataWrapper<Vec<UserMembershipsGroupItem>>>(url.as_str())
466        .await?;
467    Ok(response.data)
468}
469
470#[derive(Debug, Serialize, Deserialize, Clone, Type)]
471#[serde(rename_all = "camelCase")]
472pub struct GroupOwnershipChangeRequest {
473    pub user_id: i64,
474}
475
476/// Changes the owner of a group
477///
478/// # Error codes
479/// - 1: The group is invalid or does not exist.
480/// - 3: The user is invalid or does not exist.
481/// - 15: User is not a member of the group.
482/// - 16: The user does not have the necessary level of premium membership.
483/// - 17: You are not authorized to change the owner of this group.
484/// - 25: 2-Step Verification is required to make further transactions. Go to Settings > Security to complete 2-Step Verification.
485pub async fn change_owner(jar: &RequestJar, group_id: i64, user_id: i64) -> Result<(), Box<Error>> {
486    let url = format!(
487        "https://groups.roblox.com/v1/groups/{}/change-owner",
488        group_id
489    );
490    let request = GroupOwnershipChangeRequest { user_id };
491    jar.post_json(url.as_str(), &request).await?;
492    Ok(())
493}
494
495/// Claims the ownership of a group
496///
497/// # Error codes
498/// - 1: The group is invalid or does not exist.
499/// - 11: You are not authorized to claim this group.
500/// - 12: This group already has an owner.
501/// - 13: Too many attempts to claim groups. Please try again later.
502/// - 18: The operation is temporarily unavailable. Please try again later.
503pub async fn claim_ownership(jar: &RequestJar, group_id: i64) -> Result<(), Box<Error>> {
504    let url = format!(
505        "https://groups.roblox.com/v1/groups/{}/claim-ownership",
506        group_id
507    );
508    jar.post(url.as_str(), "".to_string()).await?;
509    Ok(())
510}