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,189 @@
1
+ """
2
+ roboat.inventory
3
+ ~~~~~~~~~~~~~~~~~~~
4
+ Inventory API — inventory.roblox.com + economy.roblox.com
5
+ Check asset ownership, list user inventory, asset details.
6
+ """
7
+
8
+ from __future__ import annotations
9
+ from dataclasses import dataclass
10
+ from typing import List, Optional
11
+ from roboat_utils.models import Page
12
+
13
+
14
+ @dataclass
15
+ class InventoryAsset:
16
+ user_asset_id: int
17
+ asset_id: int
18
+ name: str
19
+ asset_type_id: int
20
+ created: str
21
+ updated: str
22
+ serial_number: Optional[int] = None
23
+ is_tradable: bool = False
24
+ is_recent_average_price_valid: bool = False
25
+ recent_average_price: int = 0
26
+
27
+ @classmethod
28
+ def from_dict(cls, d: dict) -> "InventoryAsset":
29
+ return cls(
30
+ user_asset_id=d.get("userAssetId", 0),
31
+ asset_id=d.get("assetId", 0),
32
+ name=d.get("name", ""),
33
+ asset_type_id=d.get("assetTypeId", 0),
34
+ created=d.get("created", ""),
35
+ updated=d.get("updated", ""),
36
+ serial_number=d.get("serialNumber"),
37
+ is_tradable=d.get("isTradable", False),
38
+ is_recent_average_price_valid=d.get("isRecentAveragePriceValid", False),
39
+ recent_average_price=d.get("recentAveragePrice", 0),
40
+ )
41
+
42
+ def __str__(self) -> str:
43
+ serial = f" #{self.serial_number}" if self.serial_number else ""
44
+ tradable = " [Tradable]" if self.is_tradable else ""
45
+ rap = f" RAP: {self.recent_average_price:,}R$" if self.recent_average_price else ""
46
+ return f"{self.name}{serial}{tradable}{rap}"
47
+
48
+
49
+ class InventoryAPI:
50
+ BASE = "https://inventory.roblox.com/v1"
51
+ BASE2 = "https://inventory.roblox.com/v2"
52
+ ECON = "https://economy.roblox.com/v1"
53
+
54
+ def __init__(self, client):
55
+ self._c = client
56
+
57
+ def get_user_inventory(self, user_id: int, asset_type_ids: List[int],
58
+ limit: int = 100,
59
+ cursor: Optional[str] = None) -> Page:
60
+ """
61
+ Get a user's inventory filtered by asset type.
62
+
63
+ Common asset_type_ids:
64
+ 2 = T-Shirt 11 = Shirt 12 = Pants
65
+ 8 = Hat 17 = Head 18 = Face
66
+ 19 = Gear 25 = Body Parts 27 = Torso
67
+ 28 = Right Arm 29 = Left Arm 30 = Right Leg
68
+ 31 = Left Leg 41 = Hair 42 = Emote
69
+ 43 = Shoulder 44 = Front 45 = Back
70
+ 46 = Waist 47 = Climb 48 = Fall
71
+ 49 = Jump 50 = Run 51 = Swim
72
+ 52 = Walk 53 = Idle 54 = Animation
73
+ 61 = Bundle 62 = AnimationBundle
74
+
75
+ Returns:
76
+ Page of InventoryAsset objects.
77
+ """
78
+ params = {
79
+ "assetTypeIds": ",".join(str(i) for i in asset_type_ids),
80
+ "limit": limit,
81
+ "sortOrder": "Asc",
82
+ }
83
+ if cursor:
84
+ params["cursor"] = cursor
85
+ data = self._c._get(
86
+ f"{self.BASE}/users/{user_id}/assets/collectibles",
87
+ params=params,
88
+ )
89
+ assets = [InventoryAsset.from_dict(a) for a in data.get("data", [])]
90
+ return Page(
91
+ data=assets,
92
+ next_cursor=data.get("nextPageCursor"),
93
+ previous_cursor=data.get("previousPageCursor"),
94
+ )
95
+
96
+ def get_collectibles(self, user_id: int, limit: int = 100,
97
+ cursor: Optional[str] = None) -> Page:
98
+ """
99
+ Get all tradable/collectible (limited) items owned by a user.
100
+
101
+ Returns:
102
+ Page of InventoryAsset objects with RAP data.
103
+ """
104
+ params = {"limit": limit}
105
+ if cursor:
106
+ params["cursor"] = cursor
107
+ data = self._c._get(
108
+ f"{self.BASE}/users/{user_id}/assets/collectibles",
109
+ params=params,
110
+ )
111
+ assets = [InventoryAsset.from_dict(a) for a in data.get("data", [])]
112
+ return Page(
113
+ data=assets,
114
+ next_cursor=data.get("nextPageCursor"),
115
+ )
116
+
117
+ def owns_asset(self, user_id: int, asset_id: int) -> bool:
118
+ """Check if a user owns a specific asset. Returns True/False."""
119
+ try:
120
+ data = self._c._get(
121
+ f"{self.BASE}/users/{user_id}/items/1/{asset_id}/is-owned"
122
+ )
123
+ return bool(data)
124
+ except Exception:
125
+ return False
126
+
127
+ def owns_badge(self, user_id: int, badge_id: int) -> bool:
128
+ """Check if a user has earned a specific badge."""
129
+ try:
130
+ data = self._c._get(
131
+ f"https://badges.roblox.com/v1/users/{user_id}/badges/awarded-dates",
132
+ params={"badgeIds": badge_id},
133
+ )
134
+ return len(data.get("data", [])) > 0
135
+ except Exception:
136
+ return False
137
+
138
+ def owns_game_pass(self, user_id: int, game_pass_id: int) -> bool:
139
+ """Check if a user owns a game pass."""
140
+ try:
141
+ data = self._c._get(
142
+ f"https://inventory.roblox.com/v1/users/{user_id}/items/GamePass/{game_pass_id}/is-owned"
143
+ )
144
+ return bool(data)
145
+ except Exception:
146
+ return False
147
+
148
+ def get_user_item_types(self, user_id: int) -> List[dict]:
149
+ """Get all item type categories available in a user's inventory."""
150
+ data = self._c._get(
151
+ f"{self.BASE2}/users/{user_id}/inventory",
152
+ params={"limit": 100},
153
+ )
154
+ return data.get("data", [])
155
+
156
+ def get_user_hats(self, user_id: int, limit: int = 100) -> Page:
157
+ """Shortcut: Get all hats owned by a user."""
158
+ return self.get_user_inventory(user_id, [8], limit=limit)
159
+
160
+ def get_user_faces(self, user_id: int, limit: int = 100) -> Page:
161
+ """Shortcut: Get all faces owned by a user."""
162
+ return self.get_user_inventory(user_id, [18], limit=limit)
163
+
164
+ def get_user_gear(self, user_id: int, limit: int = 100) -> Page:
165
+ """Shortcut: Get all gear owned by a user."""
166
+ return self.get_user_inventory(user_id, [19], limit=limit)
167
+
168
+ def get_user_clothes(self, user_id: int, limit: int = 100) -> Page:
169
+ """Shortcut: Get all clothing (shirt, pants, t-shirt) owned by a user."""
170
+ return self.get_user_inventory(user_id, [2, 11, 12], limit=limit)
171
+
172
+ def get_total_rap(self, user_id: int) -> int:
173
+ """
174
+ Calculate the total Recent Average Price (RAP) of a user's limiteds.
175
+ Iterates through all pages of collectibles.
176
+
177
+ Returns:
178
+ int: Total RAP in Robux.
179
+ """
180
+ total = 0
181
+ cursor = None
182
+ while True:
183
+ page = self.get_collectibles(user_id, limit=100, cursor=cursor)
184
+ for asset in page.data:
185
+ total += asset.recent_average_price
186
+ cursor = page.next_cursor
187
+ if not cursor:
188
+ break
189
+ return total
@@ -0,0 +1,279 @@
1
+ """
2
+ roboat_utils.marketplace
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~
4
+ Marketplace tools — limited items, collectibles, UGC,
5
+ purchase history, and economy monitoring.
6
+
7
+ Example::
8
+
9
+ from roboat_utils.marketplace import MarketplaceAPI
10
+
11
+ market = MarketplaceAPI(client)
12
+ data = market.get_limited_data(1365767)
13
+ print(data)
14
+
15
+ profit = market.estimate_resale_profit(1365767, purchase_price=5000)
16
+ print(profit)
17
+
18
+ tracker = market.create_rap_tracker([1365767, 1028606])
19
+ tracker.snapshot()
20
+ changes = tracker.diff()
21
+ """
22
+
23
+ from __future__ import annotations
24
+ from dataclasses import dataclass, field
25
+ from typing import Dict, List, Optional
26
+ import time
27
+
28
+
29
+ @dataclass
30
+ class LimitedData:
31
+ asset_id: int
32
+ name: str
33
+ asset_stock: Optional[int]
34
+ sales: int
35
+ number_remaining: Optional[int]
36
+ recent_average_price: int
37
+ original_price: Optional[int]
38
+ lowest_resale_price: Optional[int]
39
+ price_data_points: List[dict] = field(default_factory=list)
40
+ volume_data_points: List[dict] = field(default_factory=list)
41
+
42
+ @classmethod
43
+ def from_resale_dict(
44
+ cls, asset_id: int, resale: dict, details: Optional[dict] = None
45
+ ) -> "LimitedData":
46
+ return cls(
47
+ asset_id=asset_id,
48
+ name=details.get("name", "") if details else "",
49
+ asset_stock=resale.get("assetStock"),
50
+ sales=resale.get("sales", 0),
51
+ number_remaining=resale.get("numberRemaining"),
52
+ recent_average_price=resale.get("recentAveragePrice", 0),
53
+ original_price=resale.get("originalPrice"),
54
+ lowest_resale_price=None,
55
+ price_data_points=resale.get("priceDataPoints", []),
56
+ volume_data_points=resale.get("volumeDataPoints", []),
57
+ )
58
+
59
+ @property
60
+ def price_trend(self) -> str:
61
+ """'rising', 'falling', or 'stable' based on recent price history."""
62
+ points = [p["value"] for p in self.price_data_points[-5:] if "value" in p]
63
+ if len(points) < 2:
64
+ return "unknown"
65
+ delta = points[-1] - points[0]
66
+ if delta > points[0] * 0.05:
67
+ return "rising"
68
+ if delta < -(points[0] * 0.05):
69
+ return "falling"
70
+ return "stable"
71
+
72
+ def __str__(self) -> str:
73
+ remaining = f"{self.number_remaining:,}" if self.number_remaining is not None else "N/A"
74
+ low = f"{self.lowest_resale_price:,}R$" if self.lowest_resale_price else "N/A"
75
+ emoji = {"rising": "📈", "falling": "📉", "stable": "➡️", "unknown": "❓"}
76
+ return (
77
+ f"💎 {self.name} [#{self.asset_id}]\n"
78
+ f" RAP : {self.recent_average_price:,}R$\n"
79
+ f" Lowest : {low}\n"
80
+ f" Sales : {self.sales:,}\n"
81
+ f" Remaining : {remaining}\n"
82
+ f" Trend : {emoji.get(self.price_trend, '❓')} {self.price_trend}"
83
+ )
84
+
85
+
86
+ @dataclass
87
+ class ResaleProfit:
88
+ asset_id: int
89
+ purchase_price: int
90
+ rap: int
91
+ lowest_resale: Optional[int]
92
+ roblox_fee: int
93
+ estimated_profit: int
94
+ roi_percent: float
95
+
96
+ def __str__(self) -> str:
97
+ sign = "+" if self.estimated_profit >= 0 else ""
98
+ return (
99
+ f"💰 Asset #{self.asset_id} Profit Estimate\n"
100
+ f" Bought at : {self.purchase_price:,}R$\n"
101
+ f" RAP : {self.rap:,}R$\n"
102
+ f" Roblox fee : -{self.roblox_fee:,}R$ (30%)\n"
103
+ f" Net profit : {sign}{self.estimated_profit:,}R$\n"
104
+ f" ROI : {self.roi_percent:+.1f}%"
105
+ )
106
+
107
+
108
+ class RAPTracker:
109
+ """
110
+ Tracks RAP changes for a set of limited assets over time.
111
+
112
+ Usage::
113
+
114
+ tracker = RAPTracker(client, [1365767, 1028606])
115
+ tracker.snapshot()
116
+ # ... wait some time ...
117
+ print(tracker.summary())
118
+ """
119
+
120
+ def __init__(self, client, asset_ids: List[int]) -> None:
121
+ self._c = client
122
+ self.asset_ids = asset_ids
123
+ self._snapshots: List[Dict[int, int]] = []
124
+ self._timestamps: List[float] = []
125
+
126
+ def snapshot(self) -> Dict[int, int]:
127
+ """Take a RAP snapshot. Returns {asset_id: rap}."""
128
+ snap: Dict[int, int] = {}
129
+ for aid in self.asset_ids:
130
+ try:
131
+ data = self._c._get(f"https://economy.roblox.com/v1/assets/{aid}/resale-data")
132
+ snap[aid] = data.get("recentAveragePrice", 0)
133
+ except Exception:
134
+ snap[aid] = 0
135
+ self._snapshots.append(snap)
136
+ self._timestamps.append(time.time())
137
+ return snap
138
+
139
+ def diff(self) -> List[dict]:
140
+ """Compare the last two snapshots."""
141
+ if len(self._snapshots) < 2:
142
+ return []
143
+ old, new = self._snapshots[-2], self._snapshots[-1]
144
+ results = []
145
+ for aid in self.asset_ids:
146
+ o = old.get(aid, 0)
147
+ n = new.get(aid, 0)
148
+ delta = n - o
149
+ pct = ((delta / o) * 100) if o > 0 else 0.0
150
+ results.append({
151
+ "asset_id": aid,
152
+ "old_rap": o,
153
+ "new_rap": n,
154
+ "change": delta,
155
+ "change_pct": round(pct, 2),
156
+ })
157
+ return sorted(results, key=lambda x: abs(x["change"]), reverse=True)
158
+
159
+ def summary(self) -> str:
160
+ """Formatted summary of the latest diff."""
161
+ changes = self.diff()
162
+ if not changes:
163
+ return "No comparison data yet. Call snapshot() at least twice."
164
+ lines = ["📊 RAP Tracker Summary", "─" * 50]
165
+ for c in changes:
166
+ sign = "+" if c["change"] >= 0 else ""
167
+ arrow = "↑" if c["change"] > 0 else ("↓" if c["change"] < 0 else "→")
168
+ lines.append(
169
+ f" {arrow} Asset #{c['asset_id']:>12} "
170
+ f"| {c['old_rap']:>8,} → {c['new_rap']:>8,}R$ "
171
+ f"| {sign}{c['change']:,}R$ ({sign}{c['change_pct']}%)"
172
+ )
173
+ return "\n".join(lines)
174
+
175
+
176
+ class MarketplaceAPI:
177
+ """
178
+ Marketplace tools for limited items, economy, and UGC.
179
+
180
+ Args:
181
+ client: A RoboatClient instance.
182
+ """
183
+
184
+ ECON = "https://economy.roblox.com/v1"
185
+ CAT = "https://catalog.roblox.com/v1"
186
+
187
+ def __init__(self, client) -> None:
188
+ self._c = client
189
+
190
+ def get_limited_data(self, asset_id: int) -> LimitedData:
191
+ """Full resale data including price history and lowest listing."""
192
+ resale = self._c._get(f"{self.ECON}/assets/{asset_id}/resale-data")
193
+ try:
194
+ details_resp = self._c._post(
195
+ f"{self.CAT}/catalog/items/details",
196
+ json={"items": [{"itemType": "Asset", "id": asset_id}]},
197
+ )
198
+ details: Optional[dict] = details_resp.get("data", [{}])[0]
199
+ except Exception:
200
+ details = None
201
+
202
+ data = LimitedData.from_resale_dict(asset_id, resale, details)
203
+
204
+ try:
205
+ sellers = self._c._get(
206
+ f"{self.ECON}/assets/{asset_id}/resellers", params={"limit": 1}
207
+ )
208
+ first = sellers.get("data", [{}])[0]
209
+ data.lowest_resale_price = first.get("price")
210
+ except Exception:
211
+ pass
212
+
213
+ return data
214
+
215
+ def get_limited_data_bulk(self, asset_ids: List[int]) -> List[LimitedData]:
216
+ """Get limited data for multiple assets."""
217
+ return [self.get_limited_data(aid) for aid in asset_ids]
218
+
219
+ def estimate_resale_profit(self, asset_id: int, purchase_price: int) -> ResaleProfit:
220
+ """Estimate profit from reselling a limited (Roblox takes 30%)."""
221
+ data = self.get_limited_data(asset_id)
222
+ sell_at = data.lowest_resale_price or data.recent_average_price
223
+ fee = int(sell_at * 0.30)
224
+ net = sell_at - fee
225
+ profit = net - purchase_price
226
+ roi = ((profit / purchase_price) * 100) if purchase_price > 0 else 0.0
227
+ return ResaleProfit(
228
+ asset_id=asset_id,
229
+ purchase_price=purchase_price,
230
+ rap=data.recent_average_price,
231
+ lowest_resale=data.lowest_resale_price,
232
+ roblox_fee=fee,
233
+ estimated_profit=profit,
234
+ roi_percent=round(roi, 2),
235
+ )
236
+
237
+ def create_rap_tracker(self, asset_ids: List[int]) -> RAPTracker:
238
+ """Create a RAP tracker for a list of limited asset IDs."""
239
+ return RAPTracker(self._c, asset_ids)
240
+
241
+ def find_underpriced_limiteds(
242
+ self, asset_ids: List[int], threshold_pct: float = 0.85
243
+ ) -> List[LimitedData]:
244
+ """
245
+ Find limiteds currently selling below their RAP.
246
+
247
+ Returns items priced below *threshold_pct* × RAP, sorted by discount depth.
248
+ """
249
+ underpriced: List[LimitedData] = []
250
+ for aid in asset_ids:
251
+ try:
252
+ data = self.get_limited_data(aid)
253
+ if data.lowest_resale_price and data.recent_average_price:
254
+ ratio = data.lowest_resale_price / data.recent_average_price
255
+ if ratio < threshold_pct:
256
+ underpriced.append(data)
257
+ except Exception:
258
+ continue
259
+ return sorted(
260
+ underpriced,
261
+ key=lambda d: (d.lowest_resale_price or 0) / max(d.recent_average_price, 1),
262
+ )
263
+
264
+ def get_user_purchase_history(
265
+ self,
266
+ user_id: int,
267
+ transaction_type: str = "Purchase",
268
+ limit: int = 25,
269
+ cursor: Optional[str] = None,
270
+ ) -> dict:
271
+ """Get purchase history for a user. Requires auth."""
272
+ self._c.require_auth("get_user_purchase_history")
273
+ params: dict = {"transactionType": transaction_type, "limit": limit}
274
+ if cursor:
275
+ params["cursor"] = cursor
276
+ return self._c._get(
277
+ f"https://economy.roblox.com/v2/users/{user_id}/transactions",
278
+ params=params,
279
+ )
@@ -0,0 +1,194 @@
1
+ """
2
+ roboat.messages
3
+ ~~~~~~~~~~~~~~~~~~
4
+ Private Messages API — privatemessages.roblox.com
5
+ All endpoints require authentication.
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 Page
12
+
13
+
14
+ @dataclass
15
+ class Message:
16
+ id: int
17
+ sender_id: int
18
+ sender_name: str
19
+ recipient_id: int
20
+ recipient_name: str
21
+ subject: str
22
+ body: str
23
+ created: str
24
+ updated: str
25
+ is_read: bool
26
+ is_system_message: bool
27
+
28
+ @classmethod
29
+ def from_dict(cls, d: dict) -> "Message":
30
+ sender = d.get("sender", {})
31
+ recipient = d.get("recipient", {})
32
+ return cls(
33
+ id=d.get("id", 0),
34
+ sender_id=sender.get("id", 0),
35
+ sender_name=sender.get("name", ""),
36
+ recipient_id=recipient.get("id", 0),
37
+ recipient_name=recipient.get("name", ""),
38
+ subject=d.get("subject", ""),
39
+ body=d.get("body", ""),
40
+ created=d.get("created", ""),
41
+ updated=d.get("updated", ""),
42
+ is_read=d.get("isRead", False),
43
+ is_system_message=d.get("isSystemMessage", False),
44
+ )
45
+
46
+ def __str__(self) -> str:
47
+ read = "📬" if not self.is_read else "📭"
48
+ return f"{read} [{self.id}] From: {self.sender_name} | {self.subject}"
49
+
50
+
51
+ class MessagesAPI:
52
+ BASE = "https://privatemessages.roblox.com/v1"
53
+
54
+ def __init__(self, client):
55
+ self._c = client
56
+
57
+ def get_messages(self, message_type: str = "Inbox",
58
+ page_size: int = 25, page_number: int = 0) -> Page:
59
+ """
60
+ Get private messages. Requires auth.
61
+
62
+ Args:
63
+ message_type: "Inbox", "Sent", "Archive"
64
+ page_size: 10, 20, 25
65
+ page_number: Zero-indexed page number.
66
+
67
+ Returns:
68
+ Page of Message objects.
69
+ """
70
+ self._c.require_auth("get_messages")
71
+ data = self._c._get(
72
+ f"{self.BASE}/messages",
73
+ params={
74
+ "messageTab": message_type,
75
+ "pageSize": page_size,
76
+ "pageNumber": page_number,
77
+ },
78
+ )
79
+ messages = [Message.from_dict(m) for m in data.get("collection", [])]
80
+ return Page(data=messages)
81
+
82
+ def get_message(self, message_id: int) -> Message:
83
+ """Get a single message by ID. Requires auth."""
84
+ self._c.require_auth("get_message")
85
+ data = self._c._get(f"{self.BASE}/messages/{message_id}")
86
+ return Message.from_dict(data)
87
+
88
+ def send_message(self, recipient_id: int, subject: str, body: str) -> dict:
89
+ """Send a private message. Requires auth."""
90
+ self._c.require_auth("send_message")
91
+ return self._c._post(
92
+ f"{self.BASE}/messages/send",
93
+ json={
94
+ "recipientId": recipient_id,
95
+ "subject": subject,
96
+ "body": body,
97
+ },
98
+ )
99
+
100
+ def get_unread_count(self) -> int:
101
+ """Get count of unread messages. Requires auth."""
102
+ self._c.require_auth("get_unread_count")
103
+ data = self._c._get(f"{self.BASE}/messages/unread/count")
104
+ return data.get("count", 0)
105
+
106
+ def mark_read(self, message_ids: List[int]) -> None:
107
+ """Mark messages as read. Requires auth."""
108
+ self._c.require_auth("mark_read")
109
+ self._c._post(
110
+ f"{self.BASE}/messages/mark-read",
111
+ json={"messageIds": message_ids},
112
+ )
113
+
114
+ def mark_unread(self, message_ids: List[int]) -> None:
115
+ """Mark messages as unread. Requires auth."""
116
+ self._c.require_auth("mark_unread")
117
+ self._c._post(
118
+ f"{self.BASE}/messages/mark-unread",
119
+ json={"messageIds": message_ids},
120
+ )
121
+
122
+ def archive(self, message_ids: List[int]) -> None:
123
+ """Archive messages. Requires auth."""
124
+ self._c.require_auth("archive")
125
+ self._c._post(
126
+ f"{self.BASE}/messages/archive",
127
+ json={"messageIds": message_ids},
128
+ )
129
+
130
+
131
+ @dataclass
132
+ class ChatConversation:
133
+ id: int
134
+ title: str
135
+ conversation_type: str
136
+ participants: List[dict] = field(default_factory=list)
137
+ last_updated: Optional[str] = None
138
+ unread_count: int = 0
139
+
140
+ @classmethod
141
+ def from_dict(cls, d: dict) -> "ChatConversation":
142
+ return cls(
143
+ id=d.get("id", 0),
144
+ title=d.get("title", ""),
145
+ conversation_type=d.get("conversationType", ""),
146
+ participants=d.get("participants", []),
147
+ last_updated=d.get("lastUpdated"),
148
+ unread_count=d.get("unreadMessageCount", 0),
149
+ )
150
+
151
+ def __str__(self) -> str:
152
+ unread = f" [{self.unread_count} unread]" if self.unread_count else ""
153
+ return f"💬 {self.title}{unread}"
154
+
155
+
156
+ class ChatAPI:
157
+ BASE = "https://chat.roblox.com/v2"
158
+
159
+ def __init__(self, client):
160
+ self._c = client
161
+
162
+ def get_conversations(self, page_size: int = 30,
163
+ page_number: int = 1) -> List[ChatConversation]:
164
+ """Get chat conversations. Requires auth."""
165
+ self._c.require_auth("get_conversations")
166
+ data = self._c._get(
167
+ f"{self.BASE}/get-user-conversations",
168
+ params={"pageSize": page_size, "pageNumber": page_number},
169
+ )
170
+ return [ChatConversation.from_dict(c) for c in (data if isinstance(data, list) else [])]
171
+
172
+ def get_unread_conversation_count(self) -> int:
173
+ """Get count of conversations with unread messages. Requires auth."""
174
+ self._c.require_auth("get_unread_conversation_count")
175
+ data = self._c._get(f"{self.BASE}/get-unread-conversation-count")
176
+ return data.get("count", 0)
177
+
178
+ def get_messages(self, conversation_id: int,
179
+ page_size: int = 30) -> list:
180
+ """Get messages in a conversation. Requires auth."""
181
+ self._c.require_auth("get_messages")
182
+ data = self._c._get(
183
+ f"{self.BASE}/get-messages",
184
+ params={"conversationId": conversation_id, "pageSize": page_size},
185
+ )
186
+ return data if isinstance(data, list) else []
187
+
188
+ def send_message(self, conversation_id: int, message: str) -> dict:
189
+ """Send a chat message. Requires auth."""
190
+ self._c.require_auth("send_message")
191
+ return self._c._post(
192
+ f"{self.BASE}/send-message",
193
+ json={"conversationId": conversation_id, "message": message},
194
+ )