mc5-api-client 1.0.16__py3-none-any.whl → 1.0.18__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.
mc5_api_client/client.py CHANGED
@@ -13,12 +13,21 @@ import time
13
13
  import json
14
14
  from typing import Optional, Dict, Any, List
15
15
  from datetime import datetime
16
+ from urllib.parse import quote
16
17
 
17
18
  import requests
18
19
  from requests.adapters import HTTPAdapter
19
20
  from urllib3.util.retry import Retry
20
21
 
21
22
  from .auth import TokenGenerator
23
+ from .telemetry import telemetry, report_error, report_usage, is_telemetry_enabled
24
+ from .squad_battle import SquadBattleMixin
25
+ from .federation import FederationMixin
26
+ from .alerts import AlertsMixin
27
+ from .account import AccountMixin
28
+ from .transfer import TransferMixin
29
+ from .platform import Platform, get_platform_config, detect_platform_from_client_id
30
+ from .storage_admin import StorageAdminMixin
22
31
  from .exceptions import (
23
32
  MC5APIError,
24
33
  AuthenticationError,
@@ -29,7 +38,7 @@ from .exceptions import (
29
38
  )
30
39
 
31
40
 
32
- class MC5Client:
41
+ class MC5Client(SquadBattleMixin, FederationMixin, AlertsMixin, AccountMixin, TransferMixin, StorageAdminMixin):
33
42
  """
34
43
  Comprehensive MC5 API client with support for all major endpoints.
35
44
  """
@@ -42,15 +51,18 @@ class MC5Client:
42
51
  "olympus": "https://eur-olympus.gameloft.com:443",
43
52
  "iris": "https://eur-iris.gameloft.com:443",
44
53
  "hermes": "https://eur-hermes.gameloft.com",
54
+ "anubisfinder": "https://eur-anubisfinder.gameloft.com:443",
45
55
  "pandora": "https://vgold-eur.gameloft.com",
46
- "game_portal": "https://app-468561b3-9ecd-4d21-8241-30ed288f4d8b.gold0009.gameloft.com"
56
+ "game_portal": "https://app-468561b3-9ecd-4d21-8241-30ed288f4d8b.gold0009.gameloft.com",
57
+ "arion": "https://eur-arion.gameloft.com"
47
58
  }
48
59
 
49
60
  def __init__(
50
61
  self,
51
62
  username: Optional[str] = None,
52
63
  password: Optional[str] = None,
53
- client_id: str = "1875:55979:6.0.0a:windows:windows",
64
+ platform: Platform = Platform.PC,
65
+ client_id: Optional[str] = None,
54
66
  auto_refresh: bool = True,
55
67
  timeout: int = 30,
56
68
  max_retries: int = 3
@@ -61,12 +73,25 @@ class MC5Client:
61
73
  Args:
62
74
  username: MC5 username
63
75
  password: MC5 password
64
- client_id: Game client identifier
76
+ platform: Platform to use (PC or Android)
77
+ client_id: Game client identifier (auto-generated if not provided)
65
78
  auto_refresh: Automatically refresh expired tokens
66
79
  timeout: Request timeout in seconds
67
80
  max_retries: Maximum number of retry attempts
68
81
  """
69
- self.client_id = client_id
82
+ # Set platform configuration
83
+ self.platform = platform
84
+ self.platform_config = get_platform_config(platform)
85
+
86
+ # Use platform-specific client_id if not provided
87
+ if client_id is None:
88
+ self.client_id = self.platform_config.get_client_id()
89
+ else:
90
+ self.client_id = client_id
91
+ # Detect platform from custom client_id
92
+ self.platform = detect_platform_from_client_id(client_id)
93
+ self.platform_config = get_platform_config(self.platform)
94
+
70
95
  self.auto_refresh = auto_refresh
71
96
  self.timeout = timeout
72
97
  self.max_retries = max_retries
@@ -92,7 +117,7 @@ class MC5Client:
92
117
 
93
118
  # Default headers
94
119
  self.session.headers.update({
95
- "User-Agent": "MC5-API-Client/1.0.0",
120
+ "User-Agent": self.platform_config.get_user_agent(),
96
121
  "Accept": "*/*",
97
122
  "Content-Type": "application/x-www-form-urlencoded",
98
123
  "Connection": "close"
@@ -122,6 +147,10 @@ class MC5Client:
122
147
  self._username = username
123
148
  self._password = password
124
149
 
150
+ # Generate platform-specific device ID if not provided
151
+ if device_id is None:
152
+ device_id = self.platform_config.generate_device_id()
153
+
125
154
  self._token_data = self.token_generator.generate_token(
126
155
  username=username,
127
156
  password=password,
@@ -173,7 +202,7 @@ class MC5Client:
173
202
  require_token: bool = True
174
203
  ) -> Dict[str, Any]:
175
204
  """
176
- Make an HTTP request with proper error handling.
205
+ Make an HTTP request with proper error handling and telemetry.
177
206
 
178
207
  Args:
179
208
  method: HTTP method (GET, POST, PUT, DELETE)
@@ -191,6 +220,13 @@ class MC5Client:
191
220
  MC5APIError: For API-related errors
192
221
  NetworkError: For network-related errors
193
222
  """
223
+ import time
224
+ start_time = time.time()
225
+ function_name = f"{method.upper()} {url.split('/')[-1]}"
226
+
227
+ # if is_telemetry_enabled():
228
+ # print(f"🔍 Debug: Making {method} request to {url}")
229
+
194
230
  if require_token:
195
231
  self._ensure_valid_token()
196
232
  # Add access token to params
@@ -214,29 +250,69 @@ class MC5Client:
214
250
  timeout=self.timeout
215
251
  )
216
252
 
253
+ duration = time.time() - start_time
254
+
217
255
  # Handle different response types
218
256
  if response.status_code == 200:
219
257
  try:
220
- return response.json()
258
+ result = response.json()
259
+ # if is_telemetry_enabled():
260
+ # print(f"✅ Debug: {method} request successful ({duration:.2f}s)")
261
+ report_usage(function_name, True, duration, {
262
+ "status_code": response.status_code,
263
+ "response_size": len(str(result))
264
+ })
265
+ return result
221
266
  except json.JSONDecodeError:
222
- return {"response": response.text}
267
+ result = {"response": response.text}
268
+ # if is_telemetry_enabled():
269
+ # print(f"✅ Debug: {method} request successful (text response, {duration:.2f}s)")
270
+ report_usage(function_name, True, duration, {
271
+ "status_code": response.status_code,
272
+ "response_type": "text"
273
+ })
274
+ return result
223
275
 
224
276
  elif response.status_code == 401:
225
- raise AuthenticationError("Unauthorized - invalid or expired token")
277
+ error = AuthenticationError("Unauthorized - invalid or expired token")
278
+ # if is_telemetry_enabled():
279
+ # print(f"❌ Debug: Authentication failed ({duration:.2f}s)")
280
+ report_error(error, f"{method} {url}", {
281
+ "status_code": response.status_code,
282
+ "duration": duration
283
+ })
284
+ raise error
226
285
 
227
286
  elif response.status_code == 403:
228
287
  try:
229
288
  error_data = response.json()
230
- raise InsufficientScopeError(error_data.get("error", "Insufficient permissions"))
289
+ error = InsufficientScopeError(error_data.get("error", "Insufficient permissions"))
231
290
  except:
232
- raise InsufficientScopeError("Insufficient permissions")
291
+ error = InsufficientScopeError("Insufficient permissions")
292
+
293
+ if is_telemetry_enabled():
294
+ print(f"❌ Debug: Permission denied ({duration:.2f}s)")
295
+ report_error(error, f"{method} {url}", {
296
+ "status_code": response.status_code,
297
+ "duration": duration
298
+ })
299
+ raise error
233
300
 
234
301
  elif response.status_code == 429:
235
302
  retry_after = response.headers.get("Retry-After")
236
- raise RateLimitError(
303
+ error = RateLimitError(
237
304
  "Rate limit exceeded",
238
305
  retry_after=int(retry_after) if retry_after else None
239
306
  )
307
+
308
+ if is_telemetry_enabled():
309
+ print(f"⏱️ Debug: Rate limit hit ({duration:.2f}s)")
310
+ report_error(error, f"{method} {url}", {
311
+ "status_code": response.status_code,
312
+ "retry_after": retry_after,
313
+ "duration": duration
314
+ })
315
+ raise error
240
316
 
241
317
  else:
242
318
  try:
@@ -245,18 +321,51 @@ class MC5Client:
245
321
  except:
246
322
  error_msg = response.text or "Unknown error"
247
323
 
248
- raise MC5APIError(
324
+ error = MC5APIError(
249
325
  f"Request failed with status {response.status_code}: {error_msg}",
250
326
  status_code=response.status_code,
251
327
  response_data=error_data if 'error_data' in locals() else {}
252
328
  )
253
-
254
- except requests.exceptions.Timeout:
255
- raise NetworkError("Request timed out")
256
- except requests.exceptions.ConnectionError:
257
- raise NetworkError("Connection failed")
329
+
330
+ if is_telemetry_enabled():
331
+ print(f" Debug: API error {response.status_code}: {error_msg} ({duration:.2f}s)")
332
+ report_error(error, f"{method} {url}", {
333
+ "status_code": response.status_code,
334
+ "error_message": error_msg,
335
+ "duration": duration
336
+ })
337
+ raise error
338
+
339
+ except requests.exceptions.Timeout as e:
340
+ error = NetworkError("Request timed out")
341
+ if is_telemetry_enabled():
342
+ print(f"⏰ Debug: Request timeout ({time.time() - start_time:.2f}s)")
343
+ report_error(error, f"{method} {url}", {
344
+ "error_type": "timeout",
345
+ "duration": time.time() - start_time
346
+ })
347
+ raise error
348
+
349
+ except requests.exceptions.ConnectionError as e:
350
+ error = NetworkError("Connection failed")
351
+ if is_telemetry_enabled():
352
+ print(f"🔌 Debug: Connection failed ({time.time() - start_time:.2f}s)")
353
+ report_error(error, f"{method} {url}", {
354
+ "error_type": "connection",
355
+ "duration": time.time() - start_time
356
+ })
357
+ raise error
358
+
258
359
  except requests.exceptions.RequestException as e:
259
- raise NetworkError(f"Network error: {str(e)}")
360
+ error = NetworkError(f"Network error: {str(e)}")
361
+ if is_telemetry_enabled():
362
+ print(f"🌐 Debug: Network error ({time.time() - start_time:.2f}s): {e}")
363
+ report_error(error, f"{method} {url}", {
364
+ "error_type": "network",
365
+ "error_message": str(e),
366
+ "duration": time.time() - start_time
367
+ })
368
+ raise error
260
369
 
261
370
  # Profile Management
262
371
 
@@ -1007,6 +1116,126 @@ class MC5Client:
1007
1116
  return self.delete_multiple_inbox_messages(message_ids)
1008
1117
  return {"status": "success", "message": "No messages to delete"}
1009
1118
 
1119
+ def remove_friend(self, target_user_id: str, message: str = "removed",
1120
+ killsig_color: str = None, killsig_name: str = None) -> Dict[str, Any]:
1121
+ """
1122
+ Send a friend removal notification message.
1123
+
1124
+ Args:
1125
+ target_user_id: Target player's user ID
1126
+ message: Message content (default: "removed")
1127
+ killsig_color: Kill signature color (optional)
1128
+ killsig_name: Kill signature name (optional)
1129
+
1130
+ Returns:
1131
+ Message result
1132
+ """
1133
+ url = f"{self.BASE_URLS['hermes']}/messages/inbox/{quote(target_user_id)}"
1134
+ headers = {
1135
+ 'Content-Type': 'application/x-www-form-urlencoded'
1136
+ }
1137
+
1138
+ data = {
1139
+ 'access_token': self._token_data["access_token"],
1140
+ 'from': self.get_profile().get('credential', ''),
1141
+ 'body': message,
1142
+ 'reply_to': target_user_id,
1143
+ '_type': 'friendRemoved',
1144
+ 'type': 'friendRemoved'
1145
+ }
1146
+
1147
+ # Add optional parameters
1148
+ if killsig_color:
1149
+ data['_killSignColor'] = killsig_color
1150
+ data['killSignColor'] = killsig_color
1151
+ if killsig_name:
1152
+ data['_killSignName'] = killsig_name
1153
+ data['killSignName'] = killsig_name
1154
+
1155
+ response = self._make_request("POST", url, data=data)
1156
+ return response
1157
+
1158
+ def delete_friend_connection(self, target_user_id: str) -> Dict[str, Any]:
1159
+ """
1160
+ Delete a friend connection (actual removal from friends list).
1161
+
1162
+ Args:
1163
+ target_user_id: Target player's user ID
1164
+
1165
+ Returns:
1166
+ Deletion result
1167
+ """
1168
+ url = f"{self.BASE_URLS['osiris']}/accounts/me/connections/friend/{quote(target_user_id)}/delete"
1169
+ headers = {
1170
+ 'Content-Type': 'application/x-www-form-urlencoded'
1171
+ }
1172
+
1173
+ data = {
1174
+ 'access_token': self._token_data["access_token"],
1175
+ 'target_credential': target_user_id
1176
+ }
1177
+
1178
+ response = self._make_request("POST", url, data=data)
1179
+ return response
1180
+
1181
+ def send_friend_request(self, target_user_id: str, alert_kairos: bool = True) -> Dict[str, Any]:
1182
+ """
1183
+ Send a friend request to another player.
1184
+
1185
+ Args:
1186
+ target_user_id: Target player's user ID
1187
+ alert_kairos: Whether to send alert notification
1188
+
1189
+ Returns:
1190
+ Friend request result
1191
+ """
1192
+ url = f"{self.BASE_URLS['osiris']}/accounts/me/connections/friend"
1193
+ headers = {
1194
+ 'Content-Type': 'application/x-www-form-urlencoded'
1195
+ }
1196
+
1197
+ data = {
1198
+ 'access_token': self._token_data["access_token"],
1199
+ 'target_credential': target_user_id,
1200
+ 'requester_credential': self.get_profile().get('credential', ''),
1201
+ 'alert_kairos': str(alert_kairos).lower()
1202
+ }
1203
+
1204
+ response = self._make_request("POST", url, data=data)
1205
+ return response
1206
+
1207
+ def get_friend_connection_status(self, target_user_id: str) -> Dict[str, Any]:
1208
+ """
1209
+ Get the connection status with a specific friend.
1210
+
1211
+ Args:
1212
+ target_user_id: Target player's user ID
1213
+
1214
+ Returns:
1215
+ Connection status information
1216
+ """
1217
+ url = f"{self.BASE_URLS['osiris']}/accounts/me/connections/friend/{quote(target_user_id)}"
1218
+ params = {
1219
+ 'access_token': self._token_data["access_token"],
1220
+ 'target_credential': target_user_id
1221
+ }
1222
+
1223
+ response = self._make_request("GET", url, params=params)
1224
+ return response
1225
+
1226
+ def accept_friend_request(self, request_id: str) -> Dict[str, Any]:
1227
+ """
1228
+ Accept an incoming friend request.
1229
+
1230
+ Args:
1231
+ request_id: Friend request ID to accept
1232
+
1233
+ Returns:
1234
+ Accept result with connection details
1235
+ """
1236
+ url = f"{self.BASE_URLS['osiris']}/accounts/me/requests/{request_id}/accept"
1237
+ return self._make_request("POST", url)
1238
+
1010
1239
  def send_squad_wall_message(self, clan_id: str, message: str, msg_type: int = 0,
1011
1240
  player_killsig: str = None, player_killsig_color: str = None,
1012
1241
  language: str = "en", activity_type: str = "user_post") -> Dict[str, Any]:
@@ -1066,20 +1295,6 @@ class MC5Client:
1066
1295
  response = self._make_request("GET", url)
1067
1296
  return response.get("friends", [])
1068
1297
 
1069
- def send_friend_request(self, credential: str) -> Dict[str, Any]:
1070
- """
1071
- Send friend request.
1072
-
1073
- Args:
1074
- credential: Target player credential
1075
-
1076
- Returns:
1077
- Friend request result
1078
- """
1079
- url = f"{self.BASE_URLS['osiris']}/accounts/me/friends"
1080
- data = {"credential": credential}
1081
- return self._make_request("POST", url, data=data)
1082
-
1083
1298
  def check_friend_status(self, credential: str) -> Dict[str, Any]:
1084
1299
  """
1085
1300
  Check friend connection status.
@@ -1139,6 +1354,136 @@ class MC5Client:
1139
1354
  return event
1140
1355
  raise MC5APIError(f"Event '{event_name}' not found")
1141
1356
 
1357
+ def sign_up_for_event(self, event_id: str) -> Dict[str, Any]:
1358
+ """
1359
+ Sign up for an event.
1360
+
1361
+ Args:
1362
+ event_id: Event ID to sign up for
1363
+
1364
+ Returns:
1365
+ Signup result
1366
+ """
1367
+ url = f"{self.BASE_URLS['osiris']}/events/{event_id}/participants/me"
1368
+ return self._make_request("POST", url)
1369
+
1370
+ def get_event_leaderboard(self, event_id: str, offset: int = 0, limit: int = 100) -> Dict[str, Any]:
1371
+ """
1372
+ Get the leaderboard for a specific event.
1373
+
1374
+ Args:
1375
+ event_id: Event ID
1376
+ offset: Pagination offset (default: 0)
1377
+ limit: Number of entries to retrieve (default: 100)
1378
+
1379
+ Returns:
1380
+ Event leaderboard data
1381
+ """
1382
+ url = f"{self.BASE_URLS['olympus']}/leaderboards/desc/Leaderboard_{event_id}"
1383
+ params = {
1384
+ "offset": str(offset),
1385
+ "limit": str(limit)
1386
+ }
1387
+ return self._make_request("GET", url, params=params)
1388
+
1389
+ def get_my_event_leaderboard_entry(self, event_id: str, limit: int = 5) -> Dict[str, Any]:
1390
+ """
1391
+ Get your entry and surrounding players in the event leaderboard.
1392
+
1393
+ Args:
1394
+ event_id: Event ID
1395
+ limit: Number of surrounding entries to show (default: 5)
1396
+
1397
+ Returns:
1398
+ Your leaderboard entry with surrounding players
1399
+ """
1400
+ url = f"{self.BASE_URLS['olympus']}/leaderboards/desc/Leaderboard_{event_id}/me"
1401
+ params = {
1402
+ "limit": str(limit)
1403
+ }
1404
+ return self._make_request("GET", url, params=params)
1405
+
1406
+ # Squad/Group Management
1407
+
1408
+ def get_squad_invitations(self) -> List[Dict[str, Any]]:
1409
+ """
1410
+ Get all pending squad invitations.
1411
+
1412
+ Returns:
1413
+ List of squad invitations
1414
+ """
1415
+ url = f"{self.BASE_URLS['osiris']}/accounts/me/requests"
1416
+ params = {
1417
+ "request_type": "group_invitation"
1418
+ }
1419
+ response = self._make_request("GET", url, params=params)
1420
+ return response if isinstance(response, list) else []
1421
+
1422
+ def accept_squad_invitation(self, group_id: str, credential: str,
1423
+ killsig_color: str = None, killsig_id: str = None,
1424
+ score: str = None, xp: str = None) -> Dict[str, Any]:
1425
+ """
1426
+ Accept a squad invitation and join the squad.
1427
+
1428
+ Args:
1429
+ group_id: Squad/group ID
1430
+ credential: Your credential (user ID)
1431
+ killsig_color: Kill signature color (optional)
1432
+ killsig_id: Kill signature ID (optional)
1433
+ score: Your score (optional)
1434
+ xp: Your XP (optional)
1435
+
1436
+ Returns:
1437
+ Accept result
1438
+ """
1439
+ url = f"{self.BASE_URLS['osiris']}/groups/{group_id}/members/{quote(credential)}"
1440
+ data = {
1441
+ "credential": credential,
1442
+ "operation": "update"
1443
+ }
1444
+
1445
+ # Add optional parameters
1446
+ if killsig_color is not None:
1447
+ data["_killsig_color"] = str(killsig_color)
1448
+ if killsig_id is not None:
1449
+ data["_killsig_id"] = killsig_id
1450
+ if score is not None:
1451
+ data["_score"] = str(score)
1452
+ if xp is not None:
1453
+ data["_xp"] = str(xp)
1454
+
1455
+ return self._make_request("POST", url, data=data)
1456
+
1457
+ def delete_squad_invitation_message(self, message_id: str) -> Dict[str, Any]:
1458
+ """
1459
+ Delete a squad invitation message after accepting/declining.
1460
+
1461
+ Args:
1462
+ message_id: Message ID to delete
1463
+
1464
+ Returns:
1465
+ Delete result
1466
+ """
1467
+ url = f"{self.BASE_URLS['hermes']}/messages/inbox/me"
1468
+ params = {
1469
+ "msgids": message_id
1470
+ }
1471
+ return self._make_request("DELETE", url, params=params)
1472
+
1473
+ def decline_squad_invitation(self, message_id: str) -> Dict[str, Any]:
1474
+ """
1475
+ Decline a squad invitation by rejecting the request.
1476
+
1477
+ Args:
1478
+ message_id: Message ID to decline/reject
1479
+
1480
+ Returns:
1481
+ Decline result
1482
+ """
1483
+ # Use the correct endpoint to actually reject the invitation
1484
+ url = f"{self.BASE_URLS['osiris']}/accounts/me/requests/{message_id}/reject"
1485
+ return self._make_request("POST", url)
1486
+
1142
1487
  # Leaderboard
1143
1488
 
1144
1489
  def get_leaderboard(self, leaderboard_type: str = "ro") -> Dict[str, Any]:
@@ -1927,6 +2272,28 @@ class MC5Client:
1927
2272
  response = self._make_request("GET", url, params=params)
1928
2273
  return response if isinstance(response, list) else []
1929
2274
 
2275
+ def get_squad_members(self, squad_id: str, offset: int = 0, limit: int = 100) -> Dict[str, Any]:
2276
+ """
2277
+ Get squad members list.
2278
+
2279
+ Args:
2280
+ squad_id: Squad ID
2281
+ offset: Offset for pagination
2282
+ limit: Number of members to retrieve
2283
+
2284
+ Returns:
2285
+ Squad members list
2286
+ """
2287
+ url = f"{self.BASE_URLS['osiris']}/groups/{squad_id}/members"
2288
+ params = {
2289
+ "access_token": self._token_data['access_token'],
2290
+ "group_id": squad_id,
2291
+ "offset": str(offset),
2292
+ "limit": str(limit)
2293
+ }
2294
+
2295
+ return self._make_request("GET", url, params=params)
2296
+
1930
2297
  def get_clan_info(self, clan_id: str) -> Dict[str, Any]:
1931
2298
  """
1932
2299
  Get detailed information about a clan.
@@ -2267,6 +2634,503 @@ class MC5Client:
2267
2634
 
2268
2635
  return parsed_stats
2269
2636
 
2637
+ def get_platform_info(self) -> Dict[str, Any]:
2638
+ """
2639
+ Get current platform information.
2640
+
2641
+ Returns:
2642
+ Platform configuration details
2643
+ """
2644
+ return {
2645
+ "platform": self.platform.value,
2646
+ "client_id": self.client_id,
2647
+ "device_info": self.platform_config.get_device_info(),
2648
+ "user_agent": self.platform_config.get_user_agent(),
2649
+ "platform_id": self.platform_config.get_platform_id()
2650
+ }
2651
+
2652
+ def switch_platform(self, platform: Platform) -> None:
2653
+ """
2654
+ Switch to a different platform.
2655
+
2656
+ Args:
2657
+ platform: New platform to use
2658
+ """
2659
+ self.platform = platform
2660
+ self.platform_config = get_platform_config(platform)
2661
+ self.client_id = self.platform_config.get_client_id()
2662
+
2663
+ # Update session headers
2664
+ self.session.headers.update({
2665
+ "User-Agent": self.platform_config.get_user_agent()
2666
+ })
2667
+
2668
+ # Reinitialize token generator with new client_id
2669
+ self.token_generator = TokenGenerator(
2670
+ client_id=self.client_id,
2671
+ timeout=self.timeout,
2672
+ max_retries=self.max_retries
2673
+ )
2674
+
2675
+ # Clear existing token
2676
+ self._token_data = None
2677
+
2678
+ def generate_platform_credential(self) -> str:
2679
+ """
2680
+ Generate platform-specific anonymous credential.
2681
+
2682
+ Returns:
2683
+ Platform-appropriate anonymous credential
2684
+ """
2685
+ if self.platform == Platform.ANDROID:
2686
+ return self.platform_config.generate_android_credential()
2687
+ else:
2688
+ # PC credential format
2689
+ import random
2690
+ import string
2691
+ return f"win8_v2_{random.randint(100000000, 999999999)}_{''.join(random.choices(string.ascii_letters + string.digits, k=20))}"
2692
+
2693
+ def subscribe_to_chat_room(self, room_id: str, language: str = "en") -> Dict[str, Any]:
2694
+ """
2695
+ Subscribe to a chat room (Android-specific).
2696
+
2697
+ Args:
2698
+ room_id: Chat room ID
2699
+ language: Language code
2700
+
2701
+ Returns:
2702
+ Chat subscription information
2703
+ """
2704
+ self._ensure_valid_token()
2705
+
2706
+ url = f"{self.BASE_URLS['arion']}/chat/rooms/{room_id}/subscribe"
2707
+ data = {
2708
+ "language": language,
2709
+ "access_token": self._token_data['access_token']
2710
+ }
2711
+
2712
+ return self._make_request("POST", url, data=data)
2713
+
2714
+ def send_squad_message(
2715
+ self,
2716
+ room_id: str,
2717
+ message: str,
2718
+ kill_sign: str = "default_killsig_03",
2719
+ kill_sign_color: str = "1212155"
2720
+ ) -> Dict[str, Any]:
2721
+ """
2722
+ Send message to squad chat room (Android-specific).
2723
+
2724
+ Args:
2725
+ room_id: Squad room ID
2726
+ message: Message content
2727
+ kill_sign: Kill signature
2728
+ kill_sign_color: Kill signature color
2729
+
2730
+ Returns:
2731
+ Message response
2732
+ """
2733
+ self._ensure_valid_token()
2734
+
2735
+ # Get chat URL from subscription
2736
+ subscription = self.subscribe_to_chat_room(room_id)
2737
+ chat_url = subscription.get('cmd_url')
2738
+
2739
+ if not chat_url:
2740
+ raise MC5APIError("Could not get chat URL from subscription")
2741
+
2742
+ data = {
2743
+ "_killSignColor": kill_sign_color,
2744
+ "_fedId": f"fed_id:{self._token_data.get('fed_id', '')}",
2745
+ "_senderTimestamp": str(int(time.time())),
2746
+ "_senderName": self._username.split(":")[-1] if ":" in self._username else "Player",
2747
+ "_anonId": self._username,
2748
+ "_killSign": kill_sign,
2749
+ "_": "wU\\UWZ", # Android signature
2750
+ "msg": message,
2751
+ "user": '{"nickname":"' + (self._username.split(":")[-1] if ":" in self._username else "Player") + '"}',
2752
+ "access_token": self._token_data['access_token']
2753
+ }
2754
+
2755
+ return self._make_request("POST", chat_url, data=data)
2756
+
2757
+ def send_global_message(
2758
+ self,
2759
+ message: str,
2760
+ nickname: str = "Player",
2761
+ kill_sign: str = "default_killsig_03"
2762
+ ) -> Dict[str, Any]:
2763
+ """
2764
+ Send message to global chat (Android-specific).
2765
+
2766
+ Args:
2767
+ message: Message content
2768
+ nickname: Player nickname
2769
+ kill_sign: Kill signature
2770
+
2771
+ Returns:
2772
+ Message response
2773
+ """
2774
+ self._ensure_valid_token()
2775
+
2776
+ # Global chat endpoint
2777
+ url = "https://eur-fedex-fsg006.gameloft.com:54435/v1/chat/channels/mc5_global.en"
2778
+
2779
+ data = {
2780
+ "_killSignColor": "1212155",
2781
+ "_fedId": f"fed_id:{self._token_data.get('fed_id', '')}",
2782
+ "_senderTimestamp": str(int(time.time())),
2783
+ "_senderName": nickname,
2784
+ "_anonId": self._username,
2785
+ "_killSign": kill_sign,
2786
+ "_": "wU\\UWZ",
2787
+ "msg": message,
2788
+ "user": '{"nickname":"' + nickname + '"}',
2789
+ "access_token": self._token_data['access_token']
2790
+ }
2791
+
2792
+ return self._make_request("POST", url, data=data)
2793
+
2794
+ def get_squad_wall(
2795
+ self,
2796
+ room_id: str,
2797
+ limit: int = 20,
2798
+ include_fields: List[str] = None
2799
+ ) -> List[Dict[str, Any]]:
2800
+ """
2801
+ Get squad wall posts (Android-specific).
2802
+
2803
+ Args:
2804
+ room_id: Squad room ID
2805
+ limit: Number of posts to retrieve
2806
+ include_fields: Fields to include in response
2807
+
2808
+ Returns:
2809
+ List of wall posts
2810
+ """
2811
+ self._ensure_valid_token()
2812
+
2813
+ if include_fields is None:
2814
+ include_fields = ["actor", "creation", "id", "text"]
2815
+
2816
+ url = f"{self.BASE_URLS['osiris']}/groups/{room_id}/wall"
2817
+ params = {
2818
+ "access_token": self._token_data['access_token'],
2819
+ "sort_type": "chronological",
2820
+ "limit": str(limit),
2821
+ "include_fields": ",".join(include_fields)
2822
+ }
2823
+
2824
+ return self._make_request("GET", url, params=params)
2825
+
2826
+ def send_private_message(
2827
+ self,
2828
+ target_credential: str,
2829
+ message: str,
2830
+ from_name: str = "Player",
2831
+ kill_sign: str = "default_killsig_03",
2832
+ kill_sign_color: str = "1212155"
2833
+ ) -> Dict[str, Any]:
2834
+ """
2835
+ Send private message (Android-specific).
2836
+
2837
+ Args:
2838
+ target_credential: Target user's credential
2839
+ message: Message content
2840
+ from_name: Sender name
2841
+ kill_sign: Kill signature
2842
+ kill_sign_color: Kill signature color
2843
+
2844
+ Returns:
2845
+ Message response
2846
+ """
2847
+ self._ensure_valid_token()
2848
+
2849
+ url = f"{self.BASE_URLS['hermes']}/messages/inbox/{target_credential}"
2850
+ data = {
2851
+ "access_token": self._token_data['access_token'],
2852
+ "alert_kairos": "true",
2853
+ "from": from_name,
2854
+ "body": message,
2855
+ "reply_to": self._username,
2856
+ "_killSignColor": kill_sign_color,
2857
+ "_killSignName": kill_sign,
2858
+ "_type": "inbox"
2859
+ }
2860
+
2861
+ return self._make_request("POST", url, data=data)
2862
+
2863
+ def check_friend_connection(self, target_credential: str) -> Dict[str, Any]:
2864
+ """
2865
+ Check friend connection status (Android-specific).
2866
+
2867
+ Args:
2868
+ target_credential: Target user's credential
2869
+
2870
+ Returns:
2871
+ Friend connection status
2872
+ """
2873
+ self._ensure_valid_token()
2874
+
2875
+ url = f"{self.BASE_URLS['osiris']}/accounts/me/connections/friend/{target_credential}"
2876
+ params = {
2877
+ "access_token": self._token_data['access_token'],
2878
+ "target_credential": target_credential
2879
+ }
2880
+
2881
+ return self._make_request("GET", url, params=params)
2882
+
2883
+ def remove_friend(
2884
+ self,
2885
+ target_credential: str
2886
+ ) -> Dict[str, Any]:
2887
+ """
2888
+ Remove friend connection (Android-specific).
2889
+
2890
+ Args:
2891
+ target_credential: Target user's credential to remove
2892
+
2893
+ Returns:
2894
+ Friend removal response
2895
+ """
2896
+ self._ensure_valid_token()
2897
+
2898
+ url = f"{self.BASE_URLS['osiris']}/accounts/me/connections/friend/{target_credential}/delete"
2899
+ data = {
2900
+ "access_token": self._token_data['access_token'],
2901
+ "target_credential": target_credential
2902
+ }
2903
+
2904
+ return self._make_request("POST", url, data=data)
2905
+
2906
+ def send_friend_removed_message(
2907
+ self,
2908
+ target_credential: str,
2909
+ message: str = "removed",
2910
+ from_name: str = "Player",
2911
+ kill_sign: str = "default_killsig_03",
2912
+ kill_sign_color: str = "1212155"
2913
+ ) -> Dict[str, Any]:
2914
+ """
2915
+ Send friend removed notification message (Android-specific).
2916
+
2917
+ Args:
2918
+ target_credential: Target user's credential
2919
+ message: Message content (default: "removed")
2920
+ from_name: Sender name
2921
+ kill_sign: Kill signature
2922
+ kill_sign_color: Kill signature color
2923
+
2924
+ Returns:
2925
+ Message response
2926
+ """
2927
+ self._ensure_valid_token()
2928
+
2929
+ url = f"{self.BASE_URLS['hermes']}/messages/inbox/{target_credential}"
2930
+ data = {
2931
+ "access_token": self._token_data['access_token'],
2932
+ "alert_kairos": "true",
2933
+ "from": from_name,
2934
+ "body": message,
2935
+ "reply_to": self._username,
2936
+ "_killSignColor": kill_sign_color,
2937
+ "_killSignName": kill_sign,
2938
+ "_type": "friendRemoved"
2939
+ }
2940
+
2941
+ return self._make_request("POST", url, data=data)
2942
+
2943
+ def update_squad_info(
2944
+ self,
2945
+ clan_id: str,
2946
+ rating: Optional[int] = None,
2947
+ score: Optional[int] = None,
2948
+ name: Optional[str] = None,
2949
+ description: Optional[str] = None,
2950
+ member_count: Optional[int] = None,
2951
+ member_limit: Optional[int] = None,
2952
+ membership: Optional[str] = None,
2953
+ logo: Optional[str] = None,
2954
+ logo_color_primary: Optional[int] = None,
2955
+ logo_color_secondary: Optional[int] = None,
2956
+ min_join_value: Optional[int] = None,
2957
+ currency: Optional[int] = None,
2958
+ active_clan_label: Optional[bool] = None
2959
+ ) -> Dict[str, Any]:
2960
+ """
2961
+ Update squad information (Android-specific).
2962
+
2963
+ Args:
2964
+ clan_id: Squad/Clan ID to update
2965
+ rating: Squad rating value
2966
+ score: Squad score (may not update in-game)
2967
+ name: Squad name
2968
+ description: Squad description
2969
+ member_count: Current member count
2970
+ member_limit: Maximum member limit
2971
+ membership: Membership type (e.g., "owner_approved")
2972
+ logo: Logo ID
2973
+ logo_color_primary: Primary logo color
2974
+ logo_color_secondary: Secondary logo color
2975
+ min_join_value: Minimum join value
2976
+ currency: Squad currency
2977
+ active_clan_label: Whether clan label is active
2978
+
2979
+ Returns:
2980
+ Update response
2981
+ """
2982
+ self._ensure_valid_token()
2983
+
2984
+ url = f"{self.BASE_URLS['osiris']}/groups/{clan_id}"
2985
+
2986
+ # Build payload with provided parameters
2987
+ payload = {
2988
+ "access_token": self._token_data['access_token'],
2989
+ "timestamp": str(int(time.time())),
2990
+ "_anonId": self._username
2991
+ }
2992
+
2993
+ # Add optional parameters if provided
2994
+ if rating is not None:
2995
+ payload["_rating"] = str(rating)
2996
+ if score is not None:
2997
+ payload["score"] = str(score)
2998
+ if name is not None:
2999
+ payload["name"] = name
3000
+ if description is not None:
3001
+ payload["description"] = description
3002
+ if member_count is not None:
3003
+ payload["member_count"] = str(member_count)
3004
+ if member_limit is not None:
3005
+ payload["member_limit"] = str(member_limit)
3006
+ if membership is not None:
3007
+ payload["membership"] = membership
3008
+ if logo is not None:
3009
+ payload["_logo"] = logo
3010
+ if logo_color_primary is not None:
3011
+ payload["_logo_clr_prim"] = str(logo_color_primary)
3012
+ if logo_color_secondary is not None:
3013
+ payload["_logo_clr_sec"] = str(logo_color_secondary)
3014
+ if min_join_value is not None:
3015
+ payload["_min_join_value"] = str(min_join_value)
3016
+ if currency is not None:
3017
+ payload["currency"] = str(currency)
3018
+ if active_clan_label is not None:
3019
+ payload["active_clan_label"] = "true" if active_clan_label else "false"
3020
+
3021
+ return self._make_request("POST", url, data=payload)
3022
+
3023
+ def get_squad_info(
3024
+ self,
3025
+ clan_id: str
3026
+ ) -> Dict[str, Any]:
3027
+ """
3028
+ Get squad information (Android-specific).
3029
+
3030
+ Args:
3031
+ clan_id: Squad/Clan ID to retrieve
3032
+
3033
+ Returns:
3034
+ Squad information
3035
+ """
3036
+ self._ensure_valid_token()
3037
+
3038
+ url = f"{self.BASE_URLS['osiris']}/groups/{clan_id}"
3039
+ params = {
3040
+ "access_token": self._token_data['access_token']
3041
+ }
3042
+
3043
+ return self._make_request("GET", url, params=params)
3044
+
3045
+ def post_to_squad_wall(
3046
+ self,
3047
+ room_id: str,
3048
+ message: str,
3049
+ kill_sign: str = "default_killsig_03",
3050
+ kill_sign_color: int = 1212155,
3051
+ language: str = "en"
3052
+ ) -> Dict[str, Any]:
3053
+ """
3054
+ Post to squad wall (Android-specific).
3055
+
3056
+ Args:
3057
+ room_id: Squad room ID
3058
+ message: Message content
3059
+ kill_sign: Kill signature
3060
+ kill_sign_color: Kill signature color
3061
+ language: Language code
3062
+
3063
+ Returns:
3064
+ Wall post response
3065
+ """
3066
+ self._ensure_valid_token()
3067
+
3068
+ url = f"{self.BASE_URLS['osiris']}/groups/{room_id}/wall"
3069
+
3070
+ post_data = {
3071
+ "msg_body": message,
3072
+ "msg_type": 0,
3073
+ "playerKillSign": kill_sign,
3074
+ "playerKillSignColor": kill_sign_color
3075
+ }
3076
+
3077
+ data = {
3078
+ "access_token": self._token_data['access_token'],
3079
+ "text": json.dumps(post_data) + "\n",
3080
+ "language": language,
3081
+ "activity_type": "user_post",
3082
+ "alert_kairos": "false"
3083
+ }
3084
+
3085
+ return self._make_request("POST", url, data=data)
3086
+
3087
+ def get_game_alias(self) -> Dict[str, Any]:
3088
+ """
3089
+ Get game alias (Android-specific).
3090
+
3091
+ Returns:
3092
+ Game alias information
3093
+ """
3094
+ self._ensure_valid_token()
3095
+
3096
+ url = f"{self.BASE_URLS['janus']}/games/mygame/alias"
3097
+ data = {
3098
+ "access_token": self._token_data['access_token']
3099
+ }
3100
+
3101
+ return self._make_request("POST", url, data=data)
3102
+
3103
+ def get_batch_profiles(
3104
+ self,
3105
+ credentials: List[str],
3106
+ include_fields: List[str] = None
3107
+ ) -> Dict[str, Any]:
3108
+ """
3109
+ Get batch profiles for multiple credentials (Android-specific).
3110
+
3111
+ Args:
3112
+ credentials: List of player credentials (fed_id:... format)
3113
+ include_fields: Fields to include in response
3114
+
3115
+ Returns:
3116
+ Batch profile data
3117
+ """
3118
+ if include_fields is None:
3119
+ include_fields = ["_game_save", "inventory"]
3120
+
3121
+ # Use game portal endpoint
3122
+ url = f"{self.BASE_URLS['game_portal']}/1924/190/public/OfficialScripts/mc5Portal.wsgi"
3123
+
3124
+ data = {
3125
+ "op_code": "get_batch_profiles",
3126
+ "client_id": self.client_id,
3127
+ "credentials": ",".join(credentials),
3128
+ "pandora": f"https://vgold-eur.gameloft.com/{self.client_id}",
3129
+ "include_fields": ",".join(include_fields)
3130
+ }
3131
+
3132
+ return self._make_request("POST", url, data=data)
3133
+
2270
3134
  def __enter__(self):
2271
3135
  """Context manager entry."""
2272
3136
  return self