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