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.
@@ -0,0 +1,517 @@
1
+ """
2
+ roboat.develop
3
+ ~~~~~~~~~~~~~~
4
+ Developer / Publishing API — develop.roblox.com + Open Cloud
5
+ """
6
+
7
+ from __future__ import annotations
8
+ from dataclasses import dataclass, field
9
+ from typing import List, Optional, Any
10
+ from roboat_utils.models import Page
11
+ import requests as _req
12
+ import json as _json
13
+
14
+
15
+ @dataclass
16
+ class Universe:
17
+ id: int
18
+ name: str
19
+ description: str
20
+ created: str
21
+ updated: str
22
+ root_place_id: int
23
+ is_archived: bool
24
+ is_active: bool
25
+ privacy_type: str
26
+ creator_type: str
27
+ creator_id: int
28
+ creator_name: str
29
+
30
+ @classmethod
31
+ def from_dict(cls, d: dict) -> "Universe":
32
+ creator = d.get("creator", {})
33
+ return cls(
34
+ id=d.get("id", 0), name=d.get("name", ""),
35
+ description=d.get("description", ""),
36
+ created=d.get("created", ""), updated=d.get("updated", ""),
37
+ root_place_id=d.get("rootPlaceId", 0),
38
+ is_archived=d.get("isArchived", False),
39
+ is_active=d.get("isActive", True),
40
+ privacy_type=d.get("privacyType", ""),
41
+ creator_type=creator.get("type", ""),
42
+ creator_id=creator.get("id", 0),
43
+ creator_name=creator.get("name", ""),
44
+ )
45
+
46
+ def __str__(self) -> str:
47
+ status = "Active" if self.is_active else "Inactive"
48
+ arch = " [Archived]" if self.is_archived else ""
49
+ return f"Universe: {self.name}{arch} [ID: {self.id}] {status}"
50
+
51
+
52
+ @dataclass
53
+ class Place:
54
+ id: int
55
+ universe_id: int
56
+ name: str
57
+ description: str
58
+ max_players: int
59
+ server_fill: str
60
+ is_root_place: bool
61
+
62
+ @classmethod
63
+ def from_dict(cls, d: dict) -> "Place":
64
+ return cls(
65
+ id=d.get("id", 0), universe_id=d.get("universeId", 0),
66
+ name=d.get("name", ""), description=d.get("description", ""),
67
+ max_players=d.get("maxPlayerCount", 0),
68
+ server_fill=d.get("socialSlotType", ""),
69
+ is_root_place=d.get("isRootPlace", False),
70
+ )
71
+
72
+
73
+ @dataclass
74
+ class DataStore:
75
+ name: str
76
+ created: str
77
+
78
+ @classmethod
79
+ def from_dict(cls, d: dict) -> "DataStore":
80
+ return cls(name=d.get("name", ""), created=d.get("createdTime", ""))
81
+
82
+
83
+ @dataclass
84
+ class PlaceVersion:
85
+ version_number: int
86
+ version_type: str
87
+ created: str
88
+ creator_id: int
89
+ creator_type: str
90
+
91
+ @classmethod
92
+ def from_dict(cls, d: dict) -> "PlaceVersion":
93
+ return cls(
94
+ version_number=d.get("versionNumber", 0),
95
+ version_type=d.get("versionType", ""),
96
+ created=d.get("created", ""),
97
+ creator_id=d.get("creatorTargetId", 0),
98
+ creator_type=d.get("creatorType", ""),
99
+ )
100
+
101
+
102
+ @dataclass
103
+ class TeamCreateMember:
104
+ user_id: int
105
+ username: str
106
+ display_name: str
107
+
108
+ @classmethod
109
+ def from_dict(cls, d: dict) -> "TeamCreateMember":
110
+ return cls(
111
+ user_id=d.get("id", 0),
112
+ username=d.get("name", ""),
113
+ display_name=d.get("displayName", ""),
114
+ )
115
+
116
+
117
+ @dataclass
118
+ class PluginInfo:
119
+ id: int
120
+ name: str
121
+ description: str
122
+ comments_enabled: bool
123
+ version_id: int
124
+
125
+ @classmethod
126
+ def from_dict(cls, d: dict) -> "PluginInfo":
127
+ return cls(
128
+ id=d.get("id", 0), name=d.get("name", ""),
129
+ description=d.get("description", ""),
130
+ comments_enabled=d.get("commentsEnabled", False),
131
+ version_id=d.get("versionId", 0),
132
+ )
133
+
134
+
135
+ class DevelopAPI:
136
+ BASE = "https://develop.roblox.com/v1"
137
+ BASE2 = "https://develop.roblox.com/v2"
138
+ CLOUD = "https://apis.roblox.com/datastores/v1"
139
+ ODS = "https://apis.roblox.com/ordered-data-stores/v1"
140
+ MSGR = "https://apis.roblox.com/messaging-service/v1"
141
+ OCCLOUD = "https://apis.roblox.com/cloud/v2"
142
+
143
+ def __init__(self, client):
144
+ self._c = client
145
+
146
+ # ── Universes ─────────────────────────────────────────────────────
147
+
148
+ def get_universes_by_user(self, user_id: int, is_archived: bool = False,
149
+ limit: int = 50, cursor: Optional[str] = None) -> Page:
150
+ params = {"isArchived": is_archived, "limit": limit}
151
+ if cursor: params["cursor"] = cursor
152
+ data = self._c._get(f"{self.BASE}/user/universes", params=params)
153
+ return Page(data=[Universe.from_dict(u) for u in data.get("data", [])],
154
+ next_cursor=data.get("nextPageCursor"))
155
+
156
+ def get_universes_by_group(self, group_id: int, is_archived: bool = False,
157
+ limit: int = 50, cursor: Optional[str] = None) -> Page:
158
+ params = {"isArchived": is_archived, "limit": limit}
159
+ if cursor: params["cursor"] = cursor
160
+ data = self._c._get(f"{self.BASE}/groups/{group_id}/universes", params=params)
161
+ return Page(data=[Universe.from_dict(u) for u in data.get("data", [])],
162
+ next_cursor=data.get("nextPageCursor"))
163
+
164
+ def get_universe(self, universe_id: int) -> Universe:
165
+ return Universe.from_dict(self._c._get(f"{self.BASE}/universes/{universe_id}"))
166
+
167
+ def get_multiverse_details(self, universe_ids: List[int]) -> List[Universe]:
168
+ data = self._c._get(
169
+ f"{self.BASE}/universes/multiget",
170
+ params={"ids": ",".join(str(i) for i in universe_ids)},
171
+ )
172
+ return [Universe.from_dict(u) for u in data.get("data", [])]
173
+
174
+ def get_universe_settings(self, universe_id: int) -> dict:
175
+ self._c.require_auth("get_universe_settings")
176
+ return self._c._get(f"{self.BASE}/universes/{universe_id}/configuration")
177
+
178
+ def update_universe_settings(self, universe_id: int, **settings) -> dict:
179
+ self._c.require_auth("update_universe_settings")
180
+ return self._c._patch(
181
+ f"{self.BASE}/universes/{universe_id}/configuration", json=settings
182
+ )
183
+
184
+ def activate_universe(self, universe_id: int) -> None:
185
+ self._c.require_auth("activate_universe")
186
+ self._c._post(f"{self.BASE}/universes/{universe_id}/activate")
187
+
188
+ def deactivate_universe(self, universe_id: int) -> None:
189
+ self._c.require_auth("deactivate_universe")
190
+ self._c._post(f"{self.BASE}/universes/{universe_id}/deactivate")
191
+
192
+ # ── Places ────────────────────────────────────────────────────────
193
+
194
+ def get_places(self, universe_id: int, limit: int = 10,
195
+ cursor: Optional[str] = None) -> Page:
196
+ params = {"limit": limit}
197
+ if cursor: params["cursor"] = cursor
198
+ data = self._c._get(f"{self.BASE}/universes/{universe_id}/places", params=params)
199
+ return Page(data=[Place.from_dict(p) for p in data.get("data", [])],
200
+ next_cursor=data.get("nextPageCursor"))
201
+
202
+ def update_place(self, place_id: int, universe_id: int,
203
+ name: Optional[str] = None,
204
+ description: Optional[str] = None,
205
+ max_players: Optional[int] = None) -> dict:
206
+ self._c.require_auth("update_place")
207
+ payload = {}
208
+ if name is not None: payload["name"] = name
209
+ if description is not None: payload["description"] = description
210
+ if max_players is not None: payload["maxPlayerCount"] = max_players
211
+ return self._c._patch(
212
+ f"{self.BASE}/universes/{universe_id}/places/{place_id}/configuration",
213
+ json=payload,
214
+ )
215
+
216
+ def get_place_versions(self, place_id: int, limit: int = 10,
217
+ cursor: Optional[str] = None) -> Page:
218
+ self._c.require_auth("get_place_versions")
219
+ params = {"limit": limit}
220
+ if cursor: params["cursor"] = cursor
221
+ data = self._c._get(f"{self.BASE}/places/{place_id}/versions", params=params)
222
+ return Page(data=[PlaceVersion.from_dict(v) for v in data.get("data", [])],
223
+ next_cursor=data.get("nextPageCursor"))
224
+
225
+ # ── Stats ─────────────────────────────────────────────────────────
226
+
227
+ def get_game_stats(self, universe_id: int, stat_type: str = "Visits",
228
+ granularity: str = "Daily",
229
+ start_time: Optional[str] = None,
230
+ end_time: Optional[str] = None) -> List[dict]:
231
+ self._c.require_auth("get_game_stats")
232
+ params = {"type": stat_type, "granularity": granularity}
233
+ if start_time: params["startTime"] = start_time
234
+ if end_time: params["endTime"] = end_time
235
+ return self._c._get(
236
+ f"{self.BASE}/universes/{universe_id}/stats", params=params
237
+ ).get("data", [])
238
+
239
+ def get_revenue_summary(self, universe_id: int, granularity: str = "Monthly") -> dict:
240
+ self._c.require_auth("get_revenue_summary")
241
+ return self._c._get(
242
+ f"{self.BASE}/universes/{universe_id}/revenue/summary/{granularity}"
243
+ )
244
+
245
+ # ── Team Create ────────────────────────────────────────────────────
246
+
247
+ def get_team_create_settings(self, universe_id: int) -> dict:
248
+ self._c.require_auth("get_team_create_settings")
249
+ return self._c._get(f"{self.BASE}/universes/{universe_id}/teamcreate")
250
+
251
+ def update_team_create(self, universe_id: int, is_enabled: bool) -> dict:
252
+ self._c.require_auth("update_team_create")
253
+ return self._c._patch(
254
+ f"{self.BASE}/universes/{universe_id}/teamcreate",
255
+ json={"isEnabled": is_enabled},
256
+ )
257
+
258
+ def get_team_create_members(self, universe_id: int, limit: int = 50,
259
+ cursor: Optional[str] = None) -> Page:
260
+ self._c.require_auth("get_team_create_members")
261
+ params = {"limit": limit}
262
+ if cursor: params["cursor"] = cursor
263
+ data = self._c._get(
264
+ f"{self.BASE}/universes/{universe_id}/teamcreate/memberships",
265
+ params=params,
266
+ )
267
+ return Page(data=[TeamCreateMember.from_dict(m) for m in data.get("data", [])],
268
+ next_cursor=data.get("nextPageCursor"))
269
+
270
+ def add_team_create_member(self, universe_id: int, user_id: int) -> None:
271
+ self._c.require_auth("add_team_create_member")
272
+ self._c._post(
273
+ f"{self.BASE}/universes/{universe_id}/teamcreate/memberships",
274
+ json={"userId": user_id},
275
+ )
276
+
277
+ def remove_team_create_member(self, universe_id: int, user_id: int) -> None:
278
+ self._c.require_auth("remove_team_create_member")
279
+ self._c._delete(
280
+ f"{self.BASE}/universes/{universe_id}/teamcreate/memberships",
281
+ params={"userId": user_id},
282
+ )
283
+
284
+ # ── Plugins ───────────────────────────────────────────────────────
285
+
286
+ def get_plugins(self, plugin_ids: List[int]) -> List[PluginInfo]:
287
+ data = self._c._get(
288
+ f"{self.BASE}/plugins",
289
+ params={"pluginIds": ",".join(str(i) for i in plugin_ids)},
290
+ )
291
+ return [PluginInfo.from_dict(p) for p in data.get("data", [])]
292
+
293
+ def update_plugin(self, plugin_id: int, name: Optional[str] = None,
294
+ description: Optional[str] = None,
295
+ comments_enabled: Optional[bool] = None) -> None:
296
+ self._c.require_auth("update_plugin")
297
+ payload = {}
298
+ if name is not None: payload["name"] = name
299
+ if description is not None: payload["description"] = description
300
+ if comments_enabled is not None: payload["commentsEnabled"] = comments_enabled
301
+ self._c._patch(f"{self.BASE}/plugins/{plugin_id}", json=payload)
302
+
303
+ # ── DataStores (Open Cloud) ────────────────────────────────────────
304
+
305
+ def _cloud_headers(self, api_key: str) -> dict:
306
+ return {"x-api-key": api_key, "Content-Type": "application/json"}
307
+
308
+ def list_datastores(self, universe_id: int, api_key: str,
309
+ prefix: str = "", limit: int = 100) -> List[DataStore]:
310
+ r = _req.get(
311
+ f"{self.CLOUD}/universes/{universe_id}/standard-datastores",
312
+ params={"prefix": prefix, "limit": limit},
313
+ headers={"x-api-key": api_key},
314
+ )
315
+ r.raise_for_status()
316
+ return [DataStore.from_dict(d) for d in r.json().get("datastores", [])]
317
+
318
+ def get_datastore_entry(self, universe_id: int, datastore_name: str,
319
+ entry_key: str, api_key: str,
320
+ scope: str = "global") -> Any:
321
+ r = _req.get(
322
+ f"{self.CLOUD}/universes/{universe_id}/standard-datastores/datastore/entries/entry",
323
+ params={"datastoreName": datastore_name, "entryKey": entry_key, "scope": scope},
324
+ headers={"x-api-key": api_key},
325
+ )
326
+ if r.status_code == 404: return None
327
+ r.raise_for_status()
328
+ try: return r.json()
329
+ except Exception: return r.text
330
+
331
+ def set_datastore_entry(self, universe_id: int, datastore_name: str,
332
+ entry_key: str, value: Any, api_key: str,
333
+ scope: str = "global",
334
+ user_ids: Optional[List[int]] = None) -> dict:
335
+ headers = {"x-api-key": api_key, "Content-Type": "application/json"}
336
+ if user_ids:
337
+ headers["roblox-entry-userids"] = _json.dumps(user_ids)
338
+ r = _req.post(
339
+ f"{self.CLOUD}/universes/{universe_id}/standard-datastores/datastore/entries/entry",
340
+ params={"datastoreName": datastore_name, "entryKey": entry_key, "scope": scope},
341
+ headers=headers,
342
+ data=_json.dumps(value),
343
+ )
344
+ r.raise_for_status()
345
+ return r.json()
346
+
347
+ def increment_datastore_entry(self, universe_id: int, datastore_name: str,
348
+ entry_key: str, delta: float,
349
+ api_key: str, scope: str = "global") -> Any:
350
+ r = _req.post(
351
+ f"{self.CLOUD}/universes/{universe_id}/standard-datastores/datastore/entries/entry/increment",
352
+ params={"datastoreName": datastore_name, "entryKey": entry_key,
353
+ "scope": scope, "incrementBy": delta},
354
+ headers={"x-api-key": api_key},
355
+ )
356
+ r.raise_for_status()
357
+ try: return r.json()
358
+ except Exception: return r.text
359
+
360
+ def delete_datastore_entry(self, universe_id: int, datastore_name: str,
361
+ entry_key: str, api_key: str,
362
+ scope: str = "global") -> None:
363
+ r = _req.delete(
364
+ f"{self.CLOUD}/universes/{universe_id}/standard-datastores/datastore/entries/entry",
365
+ params={"datastoreName": datastore_name, "entryKey": entry_key, "scope": scope},
366
+ headers={"x-api-key": api_key},
367
+ )
368
+ if r.status_code not in (200, 404): r.raise_for_status()
369
+
370
+ def list_datastore_keys(self, universe_id: int, datastore_name: str,
371
+ api_key: str, scope: str = "global",
372
+ prefix: str = "", limit: int = 100,
373
+ cursor: Optional[str] = None) -> dict:
374
+ params = {"datastoreName": datastore_name, "scope": scope,
375
+ "prefix": prefix, "limit": limit}
376
+ if cursor: params["cursor"] = cursor
377
+ r = _req.get(
378
+ f"{self.CLOUD}/universes/{universe_id}/standard-datastores/datastore/entries",
379
+ params=params, headers={"x-api-key": api_key},
380
+ )
381
+ r.raise_for_status()
382
+ return r.json()
383
+
384
+ def list_datastore_versions(self, universe_id: int, datastore_name: str,
385
+ entry_key: str, api_key: str,
386
+ scope: str = "global", limit: int = 10) -> dict:
387
+ r = _req.get(
388
+ f"{self.CLOUD}/universes/{universe_id}/standard-datastores/datastore/entries/entry/versions",
389
+ params={"datastoreName": datastore_name, "entryKey": entry_key,
390
+ "scope": scope, "limit": limit},
391
+ headers={"x-api-key": api_key},
392
+ )
393
+ r.raise_for_status()
394
+ return r.json()
395
+
396
+ # ── Ordered DataStores ─────────────────────────────────────────────
397
+
398
+ def list_ordered_datastore(self, universe_id: int, datastore_name: str,
399
+ api_key: str, scope: str = "global",
400
+ max_page_size: int = 10,
401
+ order_by: str = "desc") -> dict:
402
+ r = _req.get(
403
+ f"{self.ODS}/universes/{universe_id}/orderedDataStores/{datastore_name}/scopes/{scope}/entries",
404
+ params={"max_page_size": max_page_size, "order_by": order_by},
405
+ headers={"x-api-key": api_key},
406
+ )
407
+ r.raise_for_status()
408
+ return r.json()
409
+
410
+ def set_ordered_datastore_entry(self, universe_id: int, datastore_name: str,
411
+ entry_key: str, value: int, api_key: str,
412
+ scope: str = "global",
413
+ allow_missing: bool = True) -> dict:
414
+ params = {}
415
+ if allow_missing: params["allow_missing"] = "true"
416
+ r = _req.patch(
417
+ f"{self.ODS}/universes/{universe_id}/orderedDataStores/{datastore_name}/scopes/{scope}/entries/{entry_key}",
418
+ params=params, json={"value": value},
419
+ headers={"x-api-key": api_key, "Content-Type": "application/json"},
420
+ )
421
+ r.raise_for_status()
422
+ return r.json()
423
+
424
+ def increment_ordered_datastore(self, universe_id: int, datastore_name: str,
425
+ entry_key: str, amount: int,
426
+ api_key: str, scope: str = "global") -> dict:
427
+ r = _req.post(
428
+ f"{self.ODS}/universes/{universe_id}/orderedDataStores/{datastore_name}/scopes/{scope}/entries/{entry_key}:increment",
429
+ json={"amount": amount},
430
+ headers={"x-api-key": api_key, "Content-Type": "application/json"},
431
+ )
432
+ r.raise_for_status()
433
+ return r.json()
434
+
435
+ # ── MessagingService ───────────────────────────────────────────────
436
+
437
+ def publish_message(self, universe_id: int, topic: str,
438
+ message: str, api_key: str) -> None:
439
+ r = _req.post(
440
+ f"{self.MSGR}/universes/{universe_id}/topics/{topic}",
441
+ json={"message": message},
442
+ headers={"x-api-key": api_key, "Content-Type": "application/json"},
443
+ )
444
+ r.raise_for_status()
445
+
446
+ def broadcast_shutdown(self, universe_id: int, api_key: str,
447
+ message: str = "Server shutting down.") -> None:
448
+ self.publish_message(universe_id, "ServerShutdown", message, api_key)
449
+
450
+ def announce(self, universe_id: int, api_key: str, text: str) -> None:
451
+ self.publish_message(universe_id, "GlobalAnnouncement", text, api_key)
452
+
453
+ # ── Bans (Open Cloud) ──────────────────────────────────────────────
454
+
455
+ def ban_user(self, universe_id: int, user_id: int, api_key: str,
456
+ duration_seconds: Optional[int] = None,
457
+ display_reason: str = "You have been banned.",
458
+ private_reason: str = "",
459
+ exclude_alt_accounts: bool = False) -> dict:
460
+ payload: dict = {
461
+ "gameJoinRestriction": {
462
+ "active": True,
463
+ "displayReason": display_reason,
464
+ "privateReason": private_reason,
465
+ "excludeAltAccounts": exclude_alt_accounts,
466
+ }
467
+ }
468
+ if duration_seconds is not None:
469
+ payload["gameJoinRestriction"]["duration"] = f"{duration_seconds}s"
470
+ r = _req.patch(
471
+ f"{self.OCCLOUD}/universes/{universe_id}/user-restrictions/{user_id}",
472
+ json=payload,
473
+ headers={"x-api-key": api_key, "Content-Type": "application/json"},
474
+ )
475
+ r.raise_for_status()
476
+ return r.json()
477
+
478
+ def unban_user(self, universe_id: int, user_id: int, api_key: str) -> dict:
479
+ r = _req.patch(
480
+ f"{self.OCCLOUD}/universes/{universe_id}/user-restrictions/{user_id}",
481
+ json={"gameJoinRestriction": {"active": False}},
482
+ headers={"x-api-key": api_key, "Content-Type": "application/json"},
483
+ )
484
+ r.raise_for_status()
485
+ return r.json()
486
+
487
+ def get_ban_status(self, universe_id: int, user_id: int, api_key: str) -> dict:
488
+ r = _req.get(
489
+ f"{self.OCCLOUD}/universes/{universe_id}/user-restrictions/{user_id}",
490
+ headers={"x-api-key": api_key},
491
+ )
492
+ r.raise_for_status()
493
+ return r.json()
494
+
495
+ def list_bans(self, universe_id: int, api_key: str,
496
+ max_page_size: int = 100,
497
+ page_token: Optional[str] = None) -> dict:
498
+ params = {"maxPageSize": max_page_size}
499
+ if page_token: params["pageToken"] = page_token
500
+ r = _req.get(
501
+ f"{self.OCCLOUD}/universes/{universe_id}/user-restrictions",
502
+ params=params, headers={"x-api-key": api_key},
503
+ )
504
+ r.raise_for_status()
505
+ return r.json()
506
+
507
+ # ── Subscriptions ─────────────────────────────────────────────────
508
+
509
+ def get_subscriptions(self, universe_id: int, limit: int = 10,
510
+ cursor: Optional[str] = None) -> Page:
511
+ self._c.require_auth("get_subscriptions")
512
+ params = {"limit": limit}
513
+ if cursor: params["cursor"] = cursor
514
+ return Page.from_dict(
515
+ self._c._get(f"{self.OCCLOUD}/universes/{universe_id}/subscriptions",
516
+ params=params)
517
+ )
@@ -0,0 +1,64 @@
1
+ """
2
+ roboat.economy
3
+ ~~~~~~~~~~~~~~~~~
4
+ Economy API — economy.roblox.com
5
+ """
6
+
7
+ from typing import Optional, List
8
+ from roboat_utils.models import RobuxBalance, Transaction, ResaleData, Page
9
+
10
+
11
+ class EconomyAPI:
12
+ BASE = "https://economy.roblox.com/v1"
13
+ BASE2 = "https://economy.roblox.com/v2"
14
+
15
+ def __init__(self, client):
16
+ self._c = client
17
+
18
+ def get_robux_balance(self, user_id: int) -> RobuxBalance:
19
+ """Get Robux balance for a user (requires auth for own balance)."""
20
+ self._c.require_auth("get_robux_balance")
21
+ data = self._c._get(f"{self.BASE}/users/{user_id}/currency")
22
+ return RobuxBalance(robux=data.get("robux", 0))
23
+
24
+ def get_transactions(self, user_id: int,
25
+ transaction_type: str = "Sale",
26
+ limit: int = 25,
27
+ cursor: Optional[str] = None) -> Page:
28
+ """
29
+ Get transaction history. Requires auth.
30
+ transaction_type: "Sale", "Purchase", "AffiliateSale", "DevEx",
31
+ "GroupPayout", "AdImpressionPayout"
32
+ """
33
+ self._c.require_auth("get_transactions")
34
+ params = {"transactionType": transaction_type, "limit": limit}
35
+ if cursor:
36
+ params["cursor"] = cursor
37
+ data = self._c._get(
38
+ f"{self.BASE2}/users/{user_id}/transaction-totals",
39
+ params=params,
40
+ )
41
+ return Page.from_dict(data)
42
+
43
+ def get_asset_resale_data(self, asset_id: int) -> ResaleData:
44
+ """Get resale price history for a limited item."""
45
+ data = self._c._get(f"{self.BASE}/assets/{asset_id}/resale-data")
46
+ return ResaleData.from_dict(asset_id, data)
47
+
48
+ def get_asset_resellers(self, asset_id: int, limit: int = 10,
49
+ cursor: Optional[str] = None) -> Page:
50
+ """Get users currently reselling a limited item."""
51
+ params = {"limit": limit}
52
+ if cursor:
53
+ params["cursor"] = cursor
54
+ data = self._c._get(
55
+ f"{self.BASE}/assets/{asset_id}/resellers",
56
+ params=params,
57
+ )
58
+ return Page.from_dict(data)
59
+
60
+ def get_group_funds(self, group_id: int) -> RobuxBalance:
61
+ """Get a group's Robux balance. Requires auth + group permission."""
62
+ self._c.require_auth("get_group_funds")
63
+ data = self._c._get(f"{self.BASE}/groups/{group_id}/currency")
64
+ return RobuxBalance(robux=data.get("robux", 0))