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.
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
+ )