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/develop.py
ADDED
|
@@ -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
|
+
)
|
roboat_utils/economy.py
ADDED
|
@@ -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))
|