roboat 2.0.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/__init__.py +75 -0
- roboat/__main__.py +7 -0
- roboat/analytics.py +343 -0
- roboat/async_client.py +447 -0
- roboat/avatar.py +45 -0
- roboat/badges.py +50 -0
- roboat/catalog.py +81 -0
- roboat/client.py +297 -0
- roboat/database.py +258 -0
- roboat/develop.py +285 -0
- roboat/economy.py +64 -0
- roboat/events.py +259 -0
- roboat/exceptions.py +64 -0
- roboat/friends.py +80 -0
- roboat/games.py +220 -0
- roboat/groups.py +356 -0
- roboat/inventory.py +189 -0
- roboat/messages.py +194 -0
- roboat/models.py +534 -0
- roboat/opencloud.py +456 -0
- roboat/presence.py +49 -0
- roboat/session.py +745 -0
- roboat/thumbnails.py +94 -0
- roboat/trades.py +213 -0
- roboat/users.py +76 -0
- roboat/utils/__init__.py +5 -0
- roboat/utils/cache.py +123 -0
- roboat/utils/paginator.py +70 -0
- roboat/utils/ratelimit.py +101 -0
- roboat-2.0.0.dist-info/METADATA +505 -0
- roboat-2.0.0.dist-info/RECORD +35 -0
- roboat-2.0.0.dist-info/WHEEL +5 -0
- roboat-2.0.0.dist-info/entry_points.txt +2 -0
- roboat-2.0.0.dist-info/licenses/LICENSE +21 -0
- roboat-2.0.0.dist-info/top_level.txt +1 -0
roboat/__init__.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
roboat v2.0.0 — The best Roblox API wrapper for Python.
|
|
3
|
+
|
|
4
|
+
Quick start::
|
|
5
|
+
|
|
6
|
+
from roboat import RoboatClient, ClientBuilder
|
|
7
|
+
from roboat import AsyncRoboatClient
|
|
8
|
+
from roboat import RoboatCloudClient
|
|
9
|
+
from roboat import RoboatSession
|
|
10
|
+
from roboat.analytics import Analytics
|
|
11
|
+
|
|
12
|
+
# Sync client
|
|
13
|
+
client = (
|
|
14
|
+
ClientBuilder()
|
|
15
|
+
.set_cookie("ROBLOSECURITY")
|
|
16
|
+
.set_cache_ttl(60)
|
|
17
|
+
.set_rate_limit(10)
|
|
18
|
+
.build()
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Async client
|
|
22
|
+
async with AsyncRoboatClient(cookie="...") as c:
|
|
23
|
+
user = await c.users.get_user(156)
|
|
24
|
+
|
|
25
|
+
# Open Cloud (API key)
|
|
26
|
+
cloud = RoboatCloudClient(api_key="roblox-KEY-xxx")
|
|
27
|
+
cloud.datastores.set(123, "Store", "key", {"coins": 500})
|
|
28
|
+
|
|
29
|
+
# Terminal
|
|
30
|
+
python -m roboat
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from .client import RoboatClient, ClientBuilder
|
|
34
|
+
from .async_client import AsyncRoboatClient
|
|
35
|
+
from .opencloud import RoboatCloudClient
|
|
36
|
+
from .session import RoboatSession
|
|
37
|
+
from .database import SessionDatabase
|
|
38
|
+
from .events import EventPoller
|
|
39
|
+
from .analytics import Analytics
|
|
40
|
+
from .utils import TokenBucket, TTLCache, Paginator, retry, cached
|
|
41
|
+
|
|
42
|
+
from .models import (
|
|
43
|
+
User, UserPresence,
|
|
44
|
+
Game, GameVotes, GameServer,
|
|
45
|
+
CatalogItem, ResaleData,
|
|
46
|
+
Group, GroupRole,
|
|
47
|
+
Friend,
|
|
48
|
+
Badge,
|
|
49
|
+
Avatar, AvatarAsset, AvatarColors,
|
|
50
|
+
RobuxBalance, Transaction,
|
|
51
|
+
Page,
|
|
52
|
+
)
|
|
53
|
+
from .trades import Trade, TradeOffer, TradeAsset
|
|
54
|
+
from .messages import Message, ChatConversation
|
|
55
|
+
from .inventory import InventoryAsset
|
|
56
|
+
from .develop import Universe, Place, DataStore
|
|
57
|
+
from .groups import GroupShout, GroupPayout, GroupJoinRequest, GroupRelationship
|
|
58
|
+
|
|
59
|
+
from .exceptions import (
|
|
60
|
+
RobloxAPIError,
|
|
61
|
+
NotAuthenticatedError,
|
|
62
|
+
UserNotFoundError,
|
|
63
|
+
GameNotFoundError,
|
|
64
|
+
ItemNotFoundError,
|
|
65
|
+
GroupNotFoundError,
|
|
66
|
+
BadgeNotFoundError,
|
|
67
|
+
RateLimitedError,
|
|
68
|
+
InvalidCookieError,
|
|
69
|
+
InsufficientFundsError,
|
|
70
|
+
DatabaseError,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
__version__ = "2.0.0"
|
|
74
|
+
__author__ = "roboat contributors"
|
|
75
|
+
__license__ = "MIT"
|
roboat/__main__.py
ADDED
roboat/analytics.py
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""
|
|
2
|
+
roboat.analytics
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
High-level analytics and reporting layer.
|
|
5
|
+
Combines multiple API calls into ready-to-use summaries and comparisons.
|
|
6
|
+
|
|
7
|
+
Example::
|
|
8
|
+
|
|
9
|
+
from roboat import RoboatClient
|
|
10
|
+
from roboat.analytics import Analytics
|
|
11
|
+
|
|
12
|
+
client = RoboatClient(cookie="...")
|
|
13
|
+
an = Analytics(client)
|
|
14
|
+
|
|
15
|
+
report = an.user_report(156)
|
|
16
|
+
print(report)
|
|
17
|
+
|
|
18
|
+
comp = an.compare_games([2753915549, 286090429])
|
|
19
|
+
print(comp)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from typing import List, Optional, Dict
|
|
25
|
+
import threading
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class UserReport:
|
|
30
|
+
user_id: int
|
|
31
|
+
username: str
|
|
32
|
+
display_name: str
|
|
33
|
+
is_banned: bool
|
|
34
|
+
has_verified_badge: bool
|
|
35
|
+
friend_count: int
|
|
36
|
+
follower_count: int
|
|
37
|
+
following_count: int
|
|
38
|
+
badge_count: int
|
|
39
|
+
group_count: int
|
|
40
|
+
total_rap: int
|
|
41
|
+
collectible_count: int
|
|
42
|
+
presence_status: str
|
|
43
|
+
avatar_url: Optional[str]
|
|
44
|
+
games: List[dict] = field(default_factory=list)
|
|
45
|
+
|
|
46
|
+
def __str__(self) -> str:
|
|
47
|
+
banned = " [BANNED]" if self.is_banned else ""
|
|
48
|
+
verify = " ✓" if self.has_verified_badge else ""
|
|
49
|
+
lines = [
|
|
50
|
+
f"",
|
|
51
|
+
f" ╔══ User Report {'═'*38}╗",
|
|
52
|
+
f" ║ {self.display_name} (@{self.username}){verify}{banned}",
|
|
53
|
+
f" ║ ID : {self.user_id}",
|
|
54
|
+
f" ║ Status : {self.presence_status}",
|
|
55
|
+
f" ╠{'═'*54}╣",
|
|
56
|
+
f" ║ Friends : {self.friend_count:>10,}",
|
|
57
|
+
f" ║ Followers : {self.follower_count:>10,}",
|
|
58
|
+
f" ║ Following : {self.following_count:>10,}",
|
|
59
|
+
f" ║ Badges : {self.badge_count:>10,}",
|
|
60
|
+
f" ║ Groups : {self.group_count:>10,}",
|
|
61
|
+
f" ╠{'═'*54}╣",
|
|
62
|
+
f" ║ Total RAP : {self.total_rap:>9,}R$",
|
|
63
|
+
f" ║ Collectibles: {self.collectible_count:>10,}",
|
|
64
|
+
]
|
|
65
|
+
if self.games:
|
|
66
|
+
lines.append(f" ╠{'═'*54}╣")
|
|
67
|
+
lines.append(f" ║ Created Games ({len(self.games)}):")
|
|
68
|
+
for g in self.games[:3]:
|
|
69
|
+
lines.append(f" ║ • {g.get('name','?')[:40]}")
|
|
70
|
+
if self.avatar_url:
|
|
71
|
+
lines.append(f" ╠{'═'*54}╣")
|
|
72
|
+
lines.append(f" ║ Avatar: {self.avatar_url}")
|
|
73
|
+
lines.append(f" ╚{'═'*54}╝")
|
|
74
|
+
return "\n".join(lines)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class GameComparison:
|
|
79
|
+
games: list
|
|
80
|
+
votes: list
|
|
81
|
+
|
|
82
|
+
def __str__(self) -> str:
|
|
83
|
+
lines = [
|
|
84
|
+
"",
|
|
85
|
+
f" ╔══ Game Comparison {'═'*35}╗",
|
|
86
|
+
f" ║ {'Name':<30} {'Visits':>12} {'Playing':>8} {'👍%':>6}",
|
|
87
|
+
f" ╠{'═'*54}╣",
|
|
88
|
+
]
|
|
89
|
+
vote_map = {v.universe_id: v for v in self.votes}
|
|
90
|
+
for g in self.games:
|
|
91
|
+
v = vote_map.get(g.id)
|
|
92
|
+
ratio = f"{v.ratio}%" if v else "N/A"
|
|
93
|
+
name = g.name[:28]
|
|
94
|
+
lines.append(
|
|
95
|
+
f" ║ {name:<30} {g.visits:>12,} {g.playing:>8,} {ratio:>6}"
|
|
96
|
+
)
|
|
97
|
+
lines.append(f" ╚{'═'*54}╝")
|
|
98
|
+
return "\n".join(lines)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class GroupReport:
|
|
103
|
+
group_id: int
|
|
104
|
+
name: str
|
|
105
|
+
owner_name: Optional[str]
|
|
106
|
+
member_count: int
|
|
107
|
+
role_count: int
|
|
108
|
+
is_public: bool
|
|
109
|
+
has_verified_badge: bool
|
|
110
|
+
robux_balance: Optional[int]
|
|
111
|
+
shout: Optional[str]
|
|
112
|
+
ally_count: int
|
|
113
|
+
enemy_count: int
|
|
114
|
+
|
|
115
|
+
def __str__(self) -> str:
|
|
116
|
+
verified = " ✓" if self.has_verified_badge else ""
|
|
117
|
+
pub = "Public" if self.is_public else "Private"
|
|
118
|
+
balance = f"{self.robux_balance:,}R$" if self.robux_balance is not None else "N/A"
|
|
119
|
+
lines = [
|
|
120
|
+
f"",
|
|
121
|
+
f" ╔══ Group Report {'═'*38}╗",
|
|
122
|
+
f" ║ {self.name}{verified} [ID: {self.group_id}]",
|
|
123
|
+
f" ║ Owner : {self.owner_name}",
|
|
124
|
+
f" ║ Access : {pub}",
|
|
125
|
+
f" ╠{'═'*54}╣",
|
|
126
|
+
f" ║ Members : {self.member_count:>10,}",
|
|
127
|
+
f" ║ Roles : {self.role_count:>10,}",
|
|
128
|
+
f" ║ Allies : {self.ally_count:>10,}",
|
|
129
|
+
f" ║ Enemies : {self.enemy_count:>10,}",
|
|
130
|
+
f" ║ Balance : {balance:>10}",
|
|
131
|
+
]
|
|
132
|
+
if self.shout:
|
|
133
|
+
shout_short = self.shout[:45] + "…" if len(self.shout) > 45 else self.shout
|
|
134
|
+
lines.append(f" ╠{'═'*54}╣")
|
|
135
|
+
lines.append(f" ║ Shout: {shout_short}")
|
|
136
|
+
lines.append(f" ╚{'═'*54}╝")
|
|
137
|
+
return "\n".join(lines)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class Analytics:
|
|
141
|
+
"""
|
|
142
|
+
High-level analytics layer that aggregates multiple API calls.
|
|
143
|
+
All methods return rich dataclasses with formatted __str__ output.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
client: A RoboatClient instance.
|
|
147
|
+
|
|
148
|
+
Example::
|
|
149
|
+
|
|
150
|
+
an = Analytics(client)
|
|
151
|
+
print(an.user_report(156))
|
|
152
|
+
print(an.compare_games([2753915549, 286090429]))
|
|
153
|
+
print(an.group_report(7))
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
def __init__(self, client):
|
|
157
|
+
self._c = client
|
|
158
|
+
|
|
159
|
+
def user_report(self, user_id: int,
|
|
160
|
+
include_games: bool = True,
|
|
161
|
+
include_rap: bool = True) -> UserReport:
|
|
162
|
+
"""
|
|
163
|
+
Build a comprehensive profile report for a user.
|
|
164
|
+
Fetches user info, social counts, presence, avatar, groups,
|
|
165
|
+
badges, and optionally games + RAP in parallel.
|
|
166
|
+
"""
|
|
167
|
+
results: Dict[str, any] = {}
|
|
168
|
+
errors: Dict[str, Exception] = {}
|
|
169
|
+
|
|
170
|
+
def fetch(key, fn):
|
|
171
|
+
try:
|
|
172
|
+
results[key] = fn()
|
|
173
|
+
except Exception as e:
|
|
174
|
+
errors[key] = e
|
|
175
|
+
results[key] = None
|
|
176
|
+
|
|
177
|
+
tasks = [
|
|
178
|
+
("user", lambda: self._c.users.get_user(user_id)),
|
|
179
|
+
("friends", lambda: self._c.friends.get_friend_count(user_id)),
|
|
180
|
+
("followers", lambda: self._c.friends.get_follower_count(user_id)),
|
|
181
|
+
("following", lambda: self._c.friends.get_following_count(user_id)),
|
|
182
|
+
("presence", lambda: self._c.presence.get_presence(user_id)),
|
|
183
|
+
("groups", lambda: self._c.groups.get_user_groups(user_id)),
|
|
184
|
+
("badges", lambda: self._c.badges.get_user_badges(user_id, limit=1)),
|
|
185
|
+
("avatar_url", lambda: self._c.thumbnails.get_avatar_url(user_id)),
|
|
186
|
+
]
|
|
187
|
+
if include_games:
|
|
188
|
+
tasks.append(("games", lambda: self._c.games.get_user_games(user_id, limit=5)))
|
|
189
|
+
if include_rap:
|
|
190
|
+
tasks.append(("collectibles", lambda: self._c.inventory.get_collectibles(user_id, limit=100)))
|
|
191
|
+
|
|
192
|
+
threads = [threading.Thread(target=fetch, args=(k, fn)) for k, fn in tasks]
|
|
193
|
+
for t in threads: t.start()
|
|
194
|
+
for t in threads: t.join()
|
|
195
|
+
|
|
196
|
+
user = results.get("user")
|
|
197
|
+
if not user:
|
|
198
|
+
raise ValueError(f"Could not fetch user {user_id}")
|
|
199
|
+
|
|
200
|
+
collectibles = results.get("collectibles")
|
|
201
|
+
total_rap = sum(a.recent_average_price for a in (collectibles.data if collectibles else []))
|
|
202
|
+
collectible_count = len(collectibles.data) if collectibles else 0
|
|
203
|
+
if collectibles and collectibles.next_cursor:
|
|
204
|
+
# There are more pages — do a full count
|
|
205
|
+
try:
|
|
206
|
+
total_rap = self._c.inventory.get_total_rap(user_id)
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
games_page = results.get("games")
|
|
211
|
+
games_list = []
|
|
212
|
+
if games_page:
|
|
213
|
+
games_list = [{"name": g.name, "visits": g.visits} for g in games_page.data]
|
|
214
|
+
|
|
215
|
+
badges_page = results.get("badges")
|
|
216
|
+
# Roblox doesn't return a total count from this endpoint — use page size as proxy
|
|
217
|
+
badge_count = len(badges_page.data) if badges_page else 0
|
|
218
|
+
|
|
219
|
+
return UserReport(
|
|
220
|
+
user_id=user.id,
|
|
221
|
+
username=user.name,
|
|
222
|
+
display_name=user.display_name,
|
|
223
|
+
is_banned=user.is_banned,
|
|
224
|
+
has_verified_badge=user.has_verified_badge,
|
|
225
|
+
friend_count=results.get("friends") or 0,
|
|
226
|
+
follower_count=results.get("followers") or 0,
|
|
227
|
+
following_count=results.get("following") or 0,
|
|
228
|
+
badge_count=badge_count,
|
|
229
|
+
group_count=len(results.get("groups") or []),
|
|
230
|
+
total_rap=total_rap,
|
|
231
|
+
collectible_count=collectible_count,
|
|
232
|
+
presence_status=(results.get("presence") or type("P", (), {"status": "Unknown"})()).status,
|
|
233
|
+
avatar_url=results.get("avatar_url"),
|
|
234
|
+
games=games_list,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
def compare_games(self, universe_ids: List[int]) -> GameComparison:
|
|
238
|
+
"""
|
|
239
|
+
Fetch and compare multiple games side by side.
|
|
240
|
+
Returns a GameComparison with visit counts, player counts, and vote ratios.
|
|
241
|
+
"""
|
|
242
|
+
games = self._c.games.get_games(universe_ids)
|
|
243
|
+
votes = self._c.games.get_votes(universe_ids)
|
|
244
|
+
return GameComparison(games=games, votes=votes)
|
|
245
|
+
|
|
246
|
+
def group_report(self, group_id: int) -> GroupReport:
|
|
247
|
+
"""
|
|
248
|
+
Build a comprehensive report for a group.
|
|
249
|
+
Fetches group info, roles, shout, allies, enemies, and optionally balance.
|
|
250
|
+
"""
|
|
251
|
+
results: Dict[str, any] = {}
|
|
252
|
+
|
|
253
|
+
def fetch(key, fn):
|
|
254
|
+
try:
|
|
255
|
+
results[key] = fn()
|
|
256
|
+
except Exception:
|
|
257
|
+
results[key] = None
|
|
258
|
+
|
|
259
|
+
tasks = [
|
|
260
|
+
("group", lambda: self._c.groups.get_group(group_id)),
|
|
261
|
+
("roles", lambda: self._c.groups.get_roles(group_id)),
|
|
262
|
+
("shout", lambda: self._c.groups.get_group_shout(group_id)),
|
|
263
|
+
("allies", lambda: self._c.groups.get_allies(group_id)),
|
|
264
|
+
("enemies", lambda: self._c.groups.get_enemies(group_id)),
|
|
265
|
+
]
|
|
266
|
+
if self._c.is_authenticated:
|
|
267
|
+
tasks.append(("balance", lambda: self._c.economy.get_group_funds(group_id)))
|
|
268
|
+
|
|
269
|
+
threads = [threading.Thread(target=fetch, args=(k, fn)) for k, fn in tasks]
|
|
270
|
+
for t in threads: t.start()
|
|
271
|
+
for t in threads: t.join()
|
|
272
|
+
|
|
273
|
+
group = results.get("group")
|
|
274
|
+
if not group:
|
|
275
|
+
raise ValueError(f"Could not fetch group {group_id}")
|
|
276
|
+
|
|
277
|
+
shout = results.get("shout")
|
|
278
|
+
balance_obj = results.get("balance")
|
|
279
|
+
|
|
280
|
+
return GroupReport(
|
|
281
|
+
group_id=group.id,
|
|
282
|
+
name=group.name,
|
|
283
|
+
owner_name=group.owner_name,
|
|
284
|
+
member_count=group.member_count,
|
|
285
|
+
role_count=len(results.get("roles") or []),
|
|
286
|
+
is_public=group.is_public,
|
|
287
|
+
has_verified_badge=group.has_verified_badge,
|
|
288
|
+
robux_balance=balance_obj.robux if balance_obj else None,
|
|
289
|
+
shout=shout.body if shout else None,
|
|
290
|
+
ally_count=len(results.get("allies") or []),
|
|
291
|
+
enemy_count=len(results.get("enemies") or []),
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def leaderboard(self, universe_ids: List[int],
|
|
295
|
+
by: str = "visits") -> List[dict]:
|
|
296
|
+
"""
|
|
297
|
+
Rank games by a stat.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
by: "visits", "playing", "favorites", "ratio" (vote %)
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Sorted list of dicts with game info + stat.
|
|
304
|
+
"""
|
|
305
|
+
games = self._c.games.get_games(universe_ids)
|
|
306
|
+
votes = self._c.games.get_votes(universe_ids)
|
|
307
|
+
vmap = {v.universe_id: v for v in votes}
|
|
308
|
+
favs = {uid: self._c.games.get_favorite_count(uid) for uid in universe_ids}
|
|
309
|
+
|
|
310
|
+
rows = []
|
|
311
|
+
for g in games:
|
|
312
|
+
v = vmap.get(g.id)
|
|
313
|
+
rows.append({
|
|
314
|
+
"rank": 0,
|
|
315
|
+
"name": g.name,
|
|
316
|
+
"id": g.id,
|
|
317
|
+
"visits": g.visits,
|
|
318
|
+
"playing": g.playing,
|
|
319
|
+
"favorites": favs.get(g.id, 0),
|
|
320
|
+
"ratio": v.ratio if v else 0.0,
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
key_map = {"visits": "visits", "playing": "playing",
|
|
324
|
+
"favorites": "favorites", "ratio": "ratio"}
|
|
325
|
+
rows.sort(key=lambda r: r[key_map.get(by, "visits")], reverse=True)
|
|
326
|
+
for i, row in enumerate(rows, 1):
|
|
327
|
+
row["rank"] = i
|
|
328
|
+
return rows
|
|
329
|
+
|
|
330
|
+
def rich_leaderboard_str(self, universe_ids: List[int],
|
|
331
|
+
by: str = "visits") -> str:
|
|
332
|
+
"""Return a formatted string leaderboard table."""
|
|
333
|
+
rows = self.leaderboard(universe_ids, by=by)
|
|
334
|
+
header = f" {'#':>3} {'Name':<32} {'Visits':>12} {'Playing':>8} {'Ratio':>7}"
|
|
335
|
+
divider = " " + "─" * 68
|
|
336
|
+
lines = [f"\n 🏆 Game Leaderboard (by {by})", divider, header, divider]
|
|
337
|
+
for r in rows:
|
|
338
|
+
lines.append(
|
|
339
|
+
f" {r['rank']:>3}. {r['name'][:30]:<32} "
|
|
340
|
+
f"{r['visits']:>12,} {r['playing']:>8,} {r['ratio']:>6.1f}%"
|
|
341
|
+
)
|
|
342
|
+
lines.append(divider)
|
|
343
|
+
return "\n".join(lines)
|