robase-utils 2.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- robase_utils-2.3.0.dist-info/METADATA +663 -0
- robase_utils-2.3.0.dist-info/RECORD +40 -0
- robase_utils-2.3.0.dist-info/WHEEL +5 -0
- robase_utils-2.3.0.dist-info/entry_points.txt +2 -0
- robase_utils-2.3.0.dist-info/top_level.txt +1 -0
- roboat_utils/__init__.py +128 -0
- roboat_utils/__main__.py +5 -0
- roboat_utils/analytics.py +343 -0
- roboat_utils/async_client.py +481 -0
- roboat_utils/avatar.py +45 -0
- roboat_utils/badges.py +50 -0
- roboat_utils/catalog.py +81 -0
- roboat_utils/client.py +332 -0
- roboat_utils/database.py +258 -0
- roboat_utils/develop.py +517 -0
- roboat_utils/economy.py +64 -0
- roboat_utils/events.py +259 -0
- roboat_utils/exceptions.py +221 -0
- roboat_utils/friends.py +80 -0
- roboat_utils/games.py +220 -0
- roboat_utils/groups.py +356 -0
- roboat_utils/inventory.py +189 -0
- roboat_utils/marketplace.py +279 -0
- roboat_utils/messages.py +194 -0
- roboat_utils/models.py +520 -0
- roboat_utils/moderation.py +233 -0
- roboat_utils/notifications.py +150 -0
- roboat_utils/oauth.py +152 -0
- roboat_utils/opencloud.py +456 -0
- roboat_utils/presence.py +49 -0
- roboat_utils/publish.py +222 -0
- roboat_utils/session.py +626 -0
- roboat_utils/social.py +240 -0
- roboat_utils/thumbnails.py +94 -0
- roboat_utils/trades.py +213 -0
- roboat_utils/users.py +76 -0
- roboat_utils/utils/__init__.py +5 -0
- roboat_utils/utils/cache.py +152 -0
- roboat_utils/utils/paginator.py +70 -0
- roboat_utils/utils/ratelimit.py +128 -0
roboat_utils/games.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""
|
|
2
|
+
roboat.games
|
|
3
|
+
~~~~~~~~~~~~~~~
|
|
4
|
+
Games API — games.roblox.com
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
from roboat_utils.models import Game, GameVotes, GameServer, Page
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GamesAPI:
|
|
12
|
+
BASE = "https://games.roblox.com/v1"
|
|
13
|
+
BASE2 = "https://games.roblox.com/v2"
|
|
14
|
+
|
|
15
|
+
def __init__(self, client):
|
|
16
|
+
self._c = client
|
|
17
|
+
|
|
18
|
+
# ------------------------------------------------------------------ #
|
|
19
|
+
# Game info #
|
|
20
|
+
# ------------------------------------------------------------------ #
|
|
21
|
+
|
|
22
|
+
def get_games(self, universe_ids: List[int]) -> List[Game]:
|
|
23
|
+
"""Get full game details for one or more universe IDs."""
|
|
24
|
+
data = self._c._get(
|
|
25
|
+
f"{self.BASE}/games",
|
|
26
|
+
params={"universeIds": ",".join(str(i) for i in universe_ids)},
|
|
27
|
+
)
|
|
28
|
+
return [Game.from_dict(g) for g in data.get("data", [])]
|
|
29
|
+
|
|
30
|
+
def get_game(self, universe_id: int) -> Game:
|
|
31
|
+
"""Get details for a single game by universe ID."""
|
|
32
|
+
games = self.get_games([universe_id])
|
|
33
|
+
if not games:
|
|
34
|
+
from roboat_utils.exceptions import GameNotFoundError
|
|
35
|
+
raise GameNotFoundError(f"No game found for universe {universe_id}")
|
|
36
|
+
return games[0]
|
|
37
|
+
|
|
38
|
+
def get_universe_from_place(self, place_id: int) -> int:
|
|
39
|
+
"""Resolve a place ID to its universe ID."""
|
|
40
|
+
data = self._c._get(
|
|
41
|
+
f"https://apis.roblox.com/universes/v1/places/{place_id}/universe"
|
|
42
|
+
)
|
|
43
|
+
return data["universeId"]
|
|
44
|
+
|
|
45
|
+
def get_game_from_place(self, place_id: int) -> Game:
|
|
46
|
+
"""Get a Game by place ID (resolves universe automatically)."""
|
|
47
|
+
uid = self.get_universe_from_place(place_id)
|
|
48
|
+
return self.get_game(uid)
|
|
49
|
+
|
|
50
|
+
# ------------------------------------------------------------------ #
|
|
51
|
+
# Visits / stats #
|
|
52
|
+
# ------------------------------------------------------------------ #
|
|
53
|
+
|
|
54
|
+
def get_visits(self, universe_ids: List[int]) -> dict:
|
|
55
|
+
"""
|
|
56
|
+
Return a mapping of {universe_id: visit_count} for given IDs.
|
|
57
|
+
"""
|
|
58
|
+
games = self.get_games(universe_ids)
|
|
59
|
+
return {g.id: g.visits for g in games}
|
|
60
|
+
|
|
61
|
+
def get_game_stats(self, universe_id: int,
|
|
62
|
+
stat_type: str = "Visits",
|
|
63
|
+
granularity: str = "Daily",
|
|
64
|
+
start_time: Optional[str] = None,
|
|
65
|
+
end_time: Optional[str] = None) -> list:
|
|
66
|
+
"""
|
|
67
|
+
Time-series stats for a game.
|
|
68
|
+
|
|
69
|
+
stat_type options: "Visits", "Revenue", "Favorites",
|
|
70
|
+
"PlayerHours", "ConcurrentPlayers"
|
|
71
|
+
granularity: "Hourly", "Daily", "Monthly"
|
|
72
|
+
|
|
73
|
+
Returns list of {value, date} dicts.
|
|
74
|
+
"""
|
|
75
|
+
params = {
|
|
76
|
+
"universeId": universe_id,
|
|
77
|
+
"type": stat_type,
|
|
78
|
+
"granularity": granularity,
|
|
79
|
+
}
|
|
80
|
+
if start_time:
|
|
81
|
+
params["startTime"] = start_time
|
|
82
|
+
if end_time:
|
|
83
|
+
params["endTime"] = end_time
|
|
84
|
+
data = self._c._get(
|
|
85
|
+
f"https://develop.roblox.com/v1/universes/{universe_id}/stats",
|
|
86
|
+
params=params,
|
|
87
|
+
)
|
|
88
|
+
return data.get("data", [])
|
|
89
|
+
|
|
90
|
+
# ------------------------------------------------------------------ #
|
|
91
|
+
# Discovery / lists #
|
|
92
|
+
# ------------------------------------------------------------------ #
|
|
93
|
+
|
|
94
|
+
def get_games_page(self, genre: str = "All", limit: int = 6,
|
|
95
|
+
sort_token: Optional[str] = None) -> dict:
|
|
96
|
+
"""
|
|
97
|
+
Fetch the public games discovery page (like Roblox home).
|
|
98
|
+
Returns raw sorts[] each containing games[].
|
|
99
|
+
"""
|
|
100
|
+
params = {
|
|
101
|
+
"genre": genre,
|
|
102
|
+
"GameFilter": "All",
|
|
103
|
+
"countryRegionId": 1,
|
|
104
|
+
}
|
|
105
|
+
if sort_token:
|
|
106
|
+
params["sortToken"] = sort_token
|
|
107
|
+
return self._c._get(f"{self.BASE}/games/list", params=params)
|
|
108
|
+
|
|
109
|
+
def search_games(self, keyword: str, limit: int = 10,
|
|
110
|
+
cursor: Optional[str] = None) -> Page:
|
|
111
|
+
"""Search games by keyword."""
|
|
112
|
+
params = {"keyword": keyword, "limit": limit}
|
|
113
|
+
if cursor:
|
|
114
|
+
params["cursor"] = cursor
|
|
115
|
+
data = self._c._get(f"{self.BASE}/games/list", params=params)
|
|
116
|
+
games = [Game.from_dict(g) for g in data.get("games", [])]
|
|
117
|
+
return Page(
|
|
118
|
+
data=games,
|
|
119
|
+
next_cursor=data.get("nextPageCursor"),
|
|
120
|
+
previous_cursor=data.get("previousPageCursor"),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def get_recommended_games(self, universe_id: int, limit: int = 10) -> List[Game]:
|
|
124
|
+
"""Get games recommended based on a given universe."""
|
|
125
|
+
data = self._c._get(
|
|
126
|
+
f"{self.BASE}/games/recommendations/game/{universe_id}",
|
|
127
|
+
params={"maxRows": limit},
|
|
128
|
+
)
|
|
129
|
+
return [Game.from_dict(g) for g in data.get("games", [])]
|
|
130
|
+
|
|
131
|
+
def get_user_games(self, user_id: int, limit: int = 50,
|
|
132
|
+
cursor: Optional[str] = None) -> Page:
|
|
133
|
+
"""Get games created by a user."""
|
|
134
|
+
params = {"accessFilter": "Public", "limit": limit}
|
|
135
|
+
if cursor:
|
|
136
|
+
params["cursor"] = cursor
|
|
137
|
+
data = self._c._get(f"{self.BASE2}/users/{user_id}/games", params=params)
|
|
138
|
+
return Page.from_dict(data, Game)
|
|
139
|
+
|
|
140
|
+
def get_group_games(self, group_id: int, limit: int = 50,
|
|
141
|
+
cursor: Optional[str] = None) -> Page:
|
|
142
|
+
"""Get games created by a group."""
|
|
143
|
+
params = {"limit": limit}
|
|
144
|
+
if cursor:
|
|
145
|
+
params["cursor"] = cursor
|
|
146
|
+
data = self._c._get(f"{self.BASE2}/groups/{group_id}/games", params=params)
|
|
147
|
+
return Page.from_dict(data, Game)
|
|
148
|
+
|
|
149
|
+
# ------------------------------------------------------------------ #
|
|
150
|
+
# Servers #
|
|
151
|
+
# ------------------------------------------------------------------ #
|
|
152
|
+
|
|
153
|
+
def get_servers(self, place_id: int, server_type: str = "Public",
|
|
154
|
+
limit: int = 10, cursor: Optional[str] = None) -> Page:
|
|
155
|
+
"""
|
|
156
|
+
List active servers for a place.
|
|
157
|
+
server_type: "Public" or "Friend"
|
|
158
|
+
"""
|
|
159
|
+
params = {"serverType": server_type, "limit": limit}
|
|
160
|
+
if cursor:
|
|
161
|
+
params["cursor"] = cursor
|
|
162
|
+
data = self._c._get(
|
|
163
|
+
f"{self.BASE}/games/{place_id}/servers/{server_type}",
|
|
164
|
+
params=params,
|
|
165
|
+
)
|
|
166
|
+
servers = [GameServer.from_dict(s) for s in data.get("data", [])]
|
|
167
|
+
return Page(
|
|
168
|
+
data=servers,
|
|
169
|
+
next_cursor=data.get("nextPageCursor"),
|
|
170
|
+
previous_cursor=data.get("previousPageCursor"),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# ------------------------------------------------------------------ #
|
|
174
|
+
# Votes & favourites #
|
|
175
|
+
# ------------------------------------------------------------------ #
|
|
176
|
+
|
|
177
|
+
def get_votes(self, universe_ids: List[int]) -> List[GameVotes]:
|
|
178
|
+
"""Get upvote / downvote counts for universes."""
|
|
179
|
+
data = self._c._get(
|
|
180
|
+
f"{self.BASE}/games/votes",
|
|
181
|
+
params={"universeIds": ",".join(str(i) for i in universe_ids)},
|
|
182
|
+
)
|
|
183
|
+
return [GameVotes.from_dict(v) for v in data.get("data", [])]
|
|
184
|
+
|
|
185
|
+
def get_user_vote(self, universe_id: int) -> dict:
|
|
186
|
+
"""Get the authenticated user's vote for a game. Requires auth."""
|
|
187
|
+
self._c.require_auth("get_user_vote")
|
|
188
|
+
return self._c._get(f"{self.BASE}/games/{universe_id}/votes/user")
|
|
189
|
+
|
|
190
|
+
def get_favorite_count(self, universe_id: int) -> int:
|
|
191
|
+
"""Get how many users have favorited a game."""
|
|
192
|
+
data = self._c._get(f"{self.BASE}/games/{universe_id}/favorites/count")
|
|
193
|
+
return data.get("favoritesCount", 0)
|
|
194
|
+
|
|
195
|
+
# ------------------------------------------------------------------ #
|
|
196
|
+
# Game passes #
|
|
197
|
+
# ------------------------------------------------------------------ #
|
|
198
|
+
|
|
199
|
+
def get_game_passes(self, universe_id: int, limit: int = 10,
|
|
200
|
+
cursor: Optional[str] = None) -> Page:
|
|
201
|
+
"""Get game passes for a universe."""
|
|
202
|
+
params = {"limit": limit}
|
|
203
|
+
if cursor:
|
|
204
|
+
params["cursor"] = cursor
|
|
205
|
+
data = self._c._get(
|
|
206
|
+
f"{self.BASE}/games/{universe_id}/game-passes",
|
|
207
|
+
params=params,
|
|
208
|
+
)
|
|
209
|
+
return Page.from_dict(data)
|
|
210
|
+
|
|
211
|
+
# ------------------------------------------------------------------ #
|
|
212
|
+
# Place details #
|
|
213
|
+
# ------------------------------------------------------------------ #
|
|
214
|
+
|
|
215
|
+
def get_place_details(self, place_ids: List[int]) -> list:
|
|
216
|
+
"""Get details for one or more places."""
|
|
217
|
+
return self._c._get(
|
|
218
|
+
f"{self.BASE}/games/multiget-place-details",
|
|
219
|
+
params={"placeIds": ",".join(str(i) for i in place_ids)},
|
|
220
|
+
).get("data", [])
|
roboat_utils/groups.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""
|
|
2
|
+
roboat.groups
|
|
3
|
+
~~~~~~~~~~~~~~~~
|
|
4
|
+
Groups API — groups.roblox.com
|
|
5
|
+
Full group management: roles, members, wall, shouts, payouts, join requests, relations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
from roboat_utils.models import Group, GroupRole, Page
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class GroupShout:
|
|
16
|
+
body: str
|
|
17
|
+
poster_id: int
|
|
18
|
+
poster_name: str
|
|
19
|
+
created: str
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_dict(cls, d: dict) -> "GroupShout":
|
|
23
|
+
poster = d.get("poster", {})
|
|
24
|
+
return cls(
|
|
25
|
+
body=d.get("body", ""),
|
|
26
|
+
poster_id=poster.get("userId", 0),
|
|
27
|
+
poster_name=poster.get("username", ""),
|
|
28
|
+
created=d.get("created", ""),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def __str__(self) -> str:
|
|
32
|
+
return f"📣 {self.poster_name}: {self.body}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class GroupPayout:
|
|
37
|
+
recipient_id: int
|
|
38
|
+
recipient_name: str
|
|
39
|
+
recipient_type: str
|
|
40
|
+
amount: int
|
|
41
|
+
percentage: int
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_dict(cls, d: dict) -> "GroupPayout":
|
|
45
|
+
return cls(
|
|
46
|
+
recipient_id=d.get("user", {}).get("userId", 0),
|
|
47
|
+
recipient_name=d.get("user", {}).get("username", ""),
|
|
48
|
+
recipient_type=d.get("recipientType", "User"),
|
|
49
|
+
amount=d.get("amount", 0),
|
|
50
|
+
percentage=d.get("percentage", 0),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def __str__(self) -> str:
|
|
54
|
+
return f"💸 {self.recipient_name}: {self.amount}R$ ({self.percentage}%)"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class GroupJoinRequest:
|
|
59
|
+
requester_id: int
|
|
60
|
+
requester_name: str
|
|
61
|
+
requester_display_name: str
|
|
62
|
+
created: str
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def from_dict(cls, d: dict) -> "GroupJoinRequest":
|
|
66
|
+
requester = d.get("requester", {})
|
|
67
|
+
return cls(
|
|
68
|
+
requester_id=requester.get("userId", 0),
|
|
69
|
+
requester_name=requester.get("username", ""),
|
|
70
|
+
requester_display_name=requester.get("displayName", ""),
|
|
71
|
+
created=d.get("created", ""),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def __str__(self) -> str:
|
|
75
|
+
return f"👤 {self.requester_display_name} (@{self.requester_name}) [ID: {self.requester_id}]"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class GroupRelationship:
|
|
80
|
+
group_id: int
|
|
81
|
+
group_name: str
|
|
82
|
+
relationship_type: str
|
|
83
|
+
member_count: int
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def from_dict(cls, d: dict) -> "GroupRelationship":
|
|
87
|
+
return cls(
|
|
88
|
+
group_id=d.get("id", 0),
|
|
89
|
+
group_name=d.get("name", ""),
|
|
90
|
+
relationship_type=d.get("relationshipType", ""),
|
|
91
|
+
member_count=d.get("memberCount", 0),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class GroupsAPI:
|
|
96
|
+
BASE = "https://groups.roblox.com/v1"
|
|
97
|
+
BASE2 = "https://groups.roblox.com/v2"
|
|
98
|
+
|
|
99
|
+
def __init__(self, client):
|
|
100
|
+
self._c = client
|
|
101
|
+
|
|
102
|
+
# ── Info ──────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
def get_group(self, group_id: int) -> Group:
|
|
105
|
+
data = self._c._get(f"{self.BASE}/groups/{group_id}")
|
|
106
|
+
return Group.from_dict(data)
|
|
107
|
+
|
|
108
|
+
def get_groups_by_ids(self, group_ids: List[int]) -> List[Group]:
|
|
109
|
+
data = self._c._get(
|
|
110
|
+
f"{self.BASE}/groups",
|
|
111
|
+
params={"groupIds": ",".join(str(i) for i in group_ids)},
|
|
112
|
+
)
|
|
113
|
+
return [Group.from_dict(g.get("group", g)) for g in data.get("data", [])]
|
|
114
|
+
|
|
115
|
+
def get_group_shout(self, group_id: int) -> Optional[GroupShout]:
|
|
116
|
+
data = self._c._get(f"{self.BASE}/groups/{group_id}")
|
|
117
|
+
shout_data = data.get("shout")
|
|
118
|
+
return GroupShout.from_dict(shout_data) if shout_data else None
|
|
119
|
+
|
|
120
|
+
def post_shout(self, group_id: int, message: str) -> dict:
|
|
121
|
+
self._c.require_auth("post_shout")
|
|
122
|
+
return self._c._patch(
|
|
123
|
+
f"{self.BASE}/groups/{group_id}/status",
|
|
124
|
+
json={"message": message},
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# ── Roles & members ───────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
def get_roles(self, group_id: int) -> List[GroupRole]:
|
|
130
|
+
data = self._c._get(f"{self.BASE}/groups/{group_id}/roles")
|
|
131
|
+
roles = [GroupRole.from_dict(r) for r in data.get("roles", [])]
|
|
132
|
+
return sorted(roles, key=lambda r: r.rank, reverse=True)
|
|
133
|
+
|
|
134
|
+
def get_role_by_name(self, group_id: int, name: str) -> Optional[GroupRole]:
|
|
135
|
+
for role in self.get_roles(group_id):
|
|
136
|
+
if role.name.lower() == name.lower():
|
|
137
|
+
return role
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
def get_members(self, group_id: int, role_id: Optional[int] = None,
|
|
141
|
+
limit: int = 100, cursor: Optional[str] = None,
|
|
142
|
+
sort_order: str = "Asc") -> Page:
|
|
143
|
+
params = {"limit": limit, "sortOrder": sort_order}
|
|
144
|
+
if cursor: params["cursor"] = cursor
|
|
145
|
+
if role_id:
|
|
146
|
+
url = f"{self.BASE}/groups/{group_id}/roles/{role_id}/users"
|
|
147
|
+
else:
|
|
148
|
+
url = f"{self.BASE}/groups/{group_id}/users"
|
|
149
|
+
return Page.from_dict(self._c._get(url, params=params))
|
|
150
|
+
|
|
151
|
+
def get_member_role(self, group_id: int, user_id: int) -> Optional[dict]:
|
|
152
|
+
try:
|
|
153
|
+
data = self._c._get(f"{self.BASE2}/users/{user_id}/groups/roles")
|
|
154
|
+
for entry in data.get("data", []):
|
|
155
|
+
if entry.get("group", {}).get("id") == group_id:
|
|
156
|
+
return entry.get("role")
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
def set_member_role(self, group_id: int, user_id: int, role_id: int) -> None:
|
|
162
|
+
self._c.require_auth("set_member_role")
|
|
163
|
+
self._c._patch(
|
|
164
|
+
f"{self.BASE}/groups/{group_id}/users/{user_id}",
|
|
165
|
+
json={"roleId": role_id},
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def kick_member(self, group_id: int, user_id: int) -> None:
|
|
169
|
+
self._c.require_auth("kick_member")
|
|
170
|
+
self._c._delete(f"{self.BASE}/groups/{group_id}/users/{user_id}")
|
|
171
|
+
|
|
172
|
+
def get_member_count(self, group_id: int) -> int:
|
|
173
|
+
return self._c._get(f"{self.BASE}/groups/{group_id}").get("memberCount", 0)
|
|
174
|
+
|
|
175
|
+
def is_member(self, group_id: int, user_id: int) -> bool:
|
|
176
|
+
return self.get_member_role(group_id, user_id) is not None
|
|
177
|
+
|
|
178
|
+
# ── User groups ───────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
def get_user_groups(self, user_id: int) -> list:
|
|
181
|
+
data = self._c._get(f"{self.BASE2}/users/{user_id}/groups/roles")
|
|
182
|
+
return data.get("data", [])
|
|
183
|
+
|
|
184
|
+
def join_group(self, group_id: int) -> None:
|
|
185
|
+
self._c.require_auth("join_group")
|
|
186
|
+
self._c._post(f"{self.BASE}/groups/{group_id}/users")
|
|
187
|
+
|
|
188
|
+
def leave_group(self, group_id: int) -> None:
|
|
189
|
+
self._c.require_auth("leave_group")
|
|
190
|
+
uid = self._c.user_id()
|
|
191
|
+
self._c._delete(f"{self.BASE}/groups/{group_id}/users/{uid}")
|
|
192
|
+
|
|
193
|
+
# ── Join requests ─────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
def get_join_requests(self, group_id: int, limit: int = 100,
|
|
196
|
+
cursor: Optional[str] = None) -> Page:
|
|
197
|
+
self._c.require_auth("get_join_requests")
|
|
198
|
+
params = {"limit": limit}
|
|
199
|
+
if cursor: params["cursor"] = cursor
|
|
200
|
+
data = self._c._get(
|
|
201
|
+
f"{self.BASE}/groups/{group_id}/join-requests", params=params
|
|
202
|
+
)
|
|
203
|
+
requests = [GroupJoinRequest.from_dict(r) for r in data.get("data", [])]
|
|
204
|
+
return Page(data=requests, next_cursor=data.get("nextPageCursor"))
|
|
205
|
+
|
|
206
|
+
def accept_join_request(self, group_id: int, user_id: int) -> None:
|
|
207
|
+
self._c.require_auth("accept_join_request")
|
|
208
|
+
self._c._post(
|
|
209
|
+
f"{self.BASE}/groups/{group_id}/join-requests/users/{user_id}"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def decline_join_request(self, group_id: int, user_id: int) -> None:
|
|
213
|
+
self._c.require_auth("decline_join_request")
|
|
214
|
+
self._c._delete(
|
|
215
|
+
f"{self.BASE}/groups/{group_id}/join-requests/users/{user_id}"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def accept_all_join_requests(self, group_id: int) -> int:
|
|
219
|
+
"""Accept ALL pending join requests. Returns count accepted."""
|
|
220
|
+
count = 0
|
|
221
|
+
cursor = None
|
|
222
|
+
while True:
|
|
223
|
+
page = self.get_join_requests(group_id, limit=100, cursor=cursor)
|
|
224
|
+
for req in page.data:
|
|
225
|
+
try:
|
|
226
|
+
self.accept_join_request(group_id, req.requester_id)
|
|
227
|
+
count += 1
|
|
228
|
+
except Exception:
|
|
229
|
+
pass
|
|
230
|
+
cursor = page.next_cursor
|
|
231
|
+
if not cursor:
|
|
232
|
+
break
|
|
233
|
+
return count
|
|
234
|
+
|
|
235
|
+
# ── Wall ──────────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
def get_wall(self, group_id: int, limit: int = 100,
|
|
238
|
+
cursor: Optional[str] = None, sort_order: str = "Desc") -> Page:
|
|
239
|
+
params = {"limit": limit, "sortOrder": sort_order}
|
|
240
|
+
if cursor: params["cursor"] = cursor
|
|
241
|
+
return Page.from_dict(
|
|
242
|
+
self._c._get(f"{self.BASE}/groups/{group_id}/wall/posts", params=params)
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def post_to_wall(self, group_id: int, message: str) -> dict:
|
|
246
|
+
self._c.require_auth("post_to_wall")
|
|
247
|
+
return self._c._post(
|
|
248
|
+
f"{self.BASE}/groups/{group_id}/wall/posts", json={"body": message}
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def delete_wall_post(self, group_id: int, post_id: int) -> None:
|
|
252
|
+
self._c.require_auth("delete_wall_post")
|
|
253
|
+
self._c._delete(
|
|
254
|
+
f"{self.BASE}/groups/{group_id}/wall/posts/{post_id}"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# ── Payouts ───────────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
def get_payouts(self, group_id: int) -> List[GroupPayout]:
|
|
260
|
+
self._c.require_auth("get_payouts")
|
|
261
|
+
data = self._c._get(f"{self.BASE}/groups/{group_id}/payouts")
|
|
262
|
+
return [GroupPayout.from_dict(p) for p in data.get("data", [])]
|
|
263
|
+
|
|
264
|
+
def pay_out(self, group_id: int, user_id: int, amount: int) -> None:
|
|
265
|
+
"""One-time Robux payout to a user from group funds."""
|
|
266
|
+
self._c.require_auth("pay_out")
|
|
267
|
+
self._c._post(
|
|
268
|
+
f"{self.BASE}/groups/{group_id}/payouts",
|
|
269
|
+
json={
|
|
270
|
+
"PayoutType": "FixedAmount",
|
|
271
|
+
"Recipients": [
|
|
272
|
+
{"recipientId": user_id, "recipientType": "User", "amount": amount}
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def set_recurring_payouts(self, group_id: int, recipients: List[dict]) -> None:
|
|
278
|
+
"""Set recurring percentage payouts. percentages must sum ≤ 100."""
|
|
279
|
+
self._c.require_auth("set_recurring_payouts")
|
|
280
|
+
self._c._post(
|
|
281
|
+
f"{self.BASE}/groups/{group_id}/payouts/recurring",
|
|
282
|
+
json={
|
|
283
|
+
"PayoutType": "Percentage",
|
|
284
|
+
"Recipients": [
|
|
285
|
+
{"recipientId": r["recipientId"], "recipientType": "User",
|
|
286
|
+
"amount": r["percentage"]}
|
|
287
|
+
for r in recipients
|
|
288
|
+
],
|
|
289
|
+
},
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# ── Relationships ─────────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
def get_allies(self, group_id: int, limit: int = 10) -> List[GroupRelationship]:
|
|
295
|
+
data = self._c._get(
|
|
296
|
+
f"{self.BASE}/groups/{group_id}/relationships/allies",
|
|
297
|
+
params={"limit": limit},
|
|
298
|
+
)
|
|
299
|
+
return [GroupRelationship.from_dict(g) for g in data.get("relatedGroups", [])]
|
|
300
|
+
|
|
301
|
+
def get_enemies(self, group_id: int, limit: int = 10) -> List[GroupRelationship]:
|
|
302
|
+
data = self._c._get(
|
|
303
|
+
f"{self.BASE}/groups/{group_id}/relationships/enemies",
|
|
304
|
+
params={"limit": limit},
|
|
305
|
+
)
|
|
306
|
+
return [GroupRelationship.from_dict(g) for g in data.get("relatedGroups", [])]
|
|
307
|
+
|
|
308
|
+
# ── Search ────────────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
def search(self, keyword: str, limit: int = 10,
|
|
311
|
+
cursor: Optional[str] = None) -> Page:
|
|
312
|
+
params = {"keyword": keyword, "limit": limit}
|
|
313
|
+
if cursor: params["cursor"] = cursor
|
|
314
|
+
data = self._c._get(f"{self.BASE}/groups/search", params=params)
|
|
315
|
+
return Page(
|
|
316
|
+
data=[Group.from_dict(g) for g in data.get("data", [])],
|
|
317
|
+
next_cursor=data.get("nextPageCursor"),
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
def search_lookup(self, group_name: str) -> List[Group]:
|
|
321
|
+
data = self._c._get(
|
|
322
|
+
f"{self.BASE}/groups/search/lookup",
|
|
323
|
+
params={"groupName": group_name},
|
|
324
|
+
)
|
|
325
|
+
return [Group.from_dict(g) for g in data.get("data", [])]
|
|
326
|
+
|
|
327
|
+
# ── Audit & settings ──────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
def get_audit_log(self, group_id: int, action_type: str = "",
|
|
330
|
+
user_id: Optional[int] = None,
|
|
331
|
+
limit: int = 25, cursor: Optional[str] = None) -> Page:
|
|
332
|
+
self._c.require_auth("get_audit_log")
|
|
333
|
+
params = {"limit": limit}
|
|
334
|
+
if action_type: params["actionType"] = action_type
|
|
335
|
+
if user_id: params["userId"] = user_id
|
|
336
|
+
if cursor: params["cursor"] = cursor
|
|
337
|
+
return Page.from_dict(
|
|
338
|
+
self._c._get(f"{self.BASE}/groups/{group_id}/audit-log", params=params)
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
def get_settings(self, group_id: int) -> dict:
|
|
342
|
+
self._c.require_auth("get_settings")
|
|
343
|
+
return self._c._get(f"{self.BASE}/groups/{group_id}/settings")
|
|
344
|
+
|
|
345
|
+
def update_settings(self, group_id: int, **settings) -> dict:
|
|
346
|
+
self._c.require_auth("update_settings")
|
|
347
|
+
return self._c._patch(
|
|
348
|
+
f"{self.BASE}/groups/{group_id}/settings", json=settings
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
def get_revenue_summary(self, group_id: int,
|
|
352
|
+
frequency: str = "Monthly") -> dict:
|
|
353
|
+
self._c.require_auth("get_revenue_summary")
|
|
354
|
+
return self._c._get(
|
|
355
|
+
f"https://economy.roblox.com/v1/groups/{group_id}/revenue/summary/{frequency}"
|
|
356
|
+
)
|