mc5-api-client 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1220 @@
1
+ # ────────────[ CHIZOBA ]────────────────────────────
2
+ # | Email : chizoba2026@hotmail.com
3
+ # | File : client.py
4
+ # | License : MIT License © 2026 Chizoba
5
+ # | Brief : Main MC5 API client with comprehensive functionality
6
+ # ────────────────★─────────────────────────────────
7
+
8
+ """
9
+ Main MC5 API client providing comprehensive access to Modern Combat 5 API endpoints.
10
+ """
11
+
12
+ import time
13
+ import json
14
+ from typing import Optional, Dict, Any, List
15
+ from datetime import datetime
16
+
17
+ import requests
18
+ from requests.adapters import HTTPAdapter
19
+ from urllib3.util.retry import Retry
20
+
21
+ from .auth import TokenGenerator
22
+ from .exceptions import (
23
+ MC5APIError,
24
+ AuthenticationError,
25
+ TokenExpiredError,
26
+ RateLimitError,
27
+ InsufficientScopeError,
28
+ NetworkError
29
+ )
30
+
31
+
32
+ class MC5Client:
33
+ """
34
+ Comprehensive MC5 API client with support for all major endpoints.
35
+ """
36
+
37
+ # API endpoints
38
+ BASE_URLS = {
39
+ "auth": "https://eur-janus.gameloft.com:443",
40
+ "osiris": "https://eur-osiris.gameloft.com:443",
41
+ "olympus": "https://eur-olympus.gameloft.com:443",
42
+ "iris": "https://eur-iris.gameloft.com:443",
43
+ "hermes": "https://eur-hermes.gameloft.com",
44
+ "pandora": "https://vgold-eur.gameloft.com",
45
+ "game_portal": "https://app-468561b3-9ecd-4d21-8241-30ed288f4d8b.gold0009.gameloft.com"
46
+ }
47
+
48
+ def __init__(
49
+ self,
50
+ username: Optional[str] = None,
51
+ password: Optional[str] = None,
52
+ client_id: str = "1875:55979:6.0.0a:windows:windows",
53
+ auto_refresh: bool = True,
54
+ timeout: int = 30,
55
+ max_retries: int = 3
56
+ ):
57
+ """
58
+ Initialize the MC5 API client.
59
+
60
+ Args:
61
+ username: MC5 username
62
+ password: MC5 password
63
+ client_id: Game client identifier
64
+ auto_refresh: Automatically refresh expired tokens
65
+ timeout: Request timeout in seconds
66
+ max_retries: Maximum number of retry attempts
67
+ """
68
+ self.client_id = client_id
69
+ self.auto_refresh = auto_refresh
70
+ self.timeout = timeout
71
+ self.max_retries = max_retries
72
+
73
+ # Initialize token generator
74
+ self.token_generator = TokenGenerator(
75
+ client_id=client_id,
76
+ timeout=timeout,
77
+ max_retries=max_retries
78
+ )
79
+
80
+ # Setup HTTP session
81
+ self.session = requests.Session()
82
+ retry_strategy = Retry(
83
+ total=max_retries,
84
+ status_forcelist=[429, 500, 502, 503, 504],
85
+ allowed_methods=["HEAD", "GET", "POST", "PUT", "DELETE"],
86
+ backoff_factor=1
87
+ )
88
+ adapter = HTTPAdapter(max_retries=retry_strategy)
89
+ self.session.mount("http://", adapter)
90
+ self.session.mount("https://", adapter)
91
+
92
+ # Default headers
93
+ self.session.headers.update({
94
+ "User-Agent": "MC5-API-Client/1.0.0",
95
+ "Accept": "*/*",
96
+ "Content-Type": "application/x-www-form-urlencoded",
97
+ "Connection": "close"
98
+ })
99
+
100
+ # Token storage
101
+ self._token_data = None
102
+ self._username = username
103
+ self._password = password
104
+
105
+ # Auto-authenticate if credentials provided
106
+ if username and password:
107
+ self.authenticate(username, password)
108
+
109
+ def authenticate(self, username: str, password: str, device_id: Optional[str] = None) -> Dict[str, Any]:
110
+ """
111
+ Authenticate and obtain access token.
112
+
113
+ Args:
114
+ username: MC5 username
115
+ password: MC5 password
116
+ device_id: Device ID (generated if not provided)
117
+
118
+ Returns:
119
+ Token data dictionary
120
+ """
121
+ self._username = username
122
+ self._password = password
123
+
124
+ self._token_data = self.token_generator.generate_token(
125
+ username=username,
126
+ password=password,
127
+ device_id=device_id
128
+ )
129
+
130
+ return self._token_data
131
+
132
+ def authenticate_admin(self, device_id: Optional[str] = None) -> Dict[str, Any]:
133
+ """
134
+ Authenticate as admin.
135
+
136
+ Args:
137
+ device_id: Device ID (generated if not provided)
138
+
139
+ Returns:
140
+ Admin token data dictionary
141
+ """
142
+ self._token_data = self.token_generator.generate_admin_token(device_id=device_id)
143
+ self._username = "game:mc5_system"
144
+ self._password = "admin"
145
+
146
+ return self._token_data
147
+
148
+ def _ensure_valid_token(self):
149
+ """Ensure we have a valid access token."""
150
+ if not self._token_data:
151
+ raise AuthenticationError("No authentication token available. Call authenticate() first.")
152
+
153
+ if not self.token_generator.validate_token(self._token_data):
154
+ if self.auto_refresh and self._username and self._password:
155
+ print("🟠 Token expired, refreshing...")
156
+ self._token_data = self.token_generator.refresh_token_if_needed(
157
+ self._token_data,
158
+ self._username,
159
+ self._password
160
+ )
161
+ else:
162
+ raise TokenExpiredError("Access token has expired")
163
+
164
+ def _make_request(
165
+ self,
166
+ method: str,
167
+ url: str,
168
+ params: Optional[Dict[str, Any]] = None,
169
+ data: Optional[Dict[str, Any]] = None,
170
+ json_data: Optional[Dict[str, Any]] = None,
171
+ headers: Optional[Dict[str, str]] = None,
172
+ require_token: bool = True
173
+ ) -> Dict[str, Any]:
174
+ """
175
+ Make an HTTP request with proper error handling.
176
+
177
+ Args:
178
+ method: HTTP method (GET, POST, PUT, DELETE)
179
+ url: Request URL
180
+ params: Query parameters
181
+ data: Form data
182
+ json_data: JSON data
183
+ headers: Additional headers
184
+ require_token: Whether authentication token is required
185
+
186
+ Returns:
187
+ Response JSON data
188
+
189
+ Raises:
190
+ MC5APIError: For API-related errors
191
+ NetworkError: For network-related errors
192
+ """
193
+ if require_token:
194
+ self._ensure_valid_token()
195
+ # Add access token to params
196
+ if params is None:
197
+ params = {}
198
+ params["access_token"] = self._token_data["access_token"]
199
+
200
+ # Prepare request
201
+ request_headers = self.session.headers.copy()
202
+ if headers:
203
+ request_headers.update(headers)
204
+
205
+ try:
206
+ response = self.session.request(
207
+ method=method,
208
+ url=url,
209
+ params=params,
210
+ data=data,
211
+ json=json_data,
212
+ headers=request_headers,
213
+ timeout=self.timeout
214
+ )
215
+
216
+ # Handle different response types
217
+ if response.status_code == 200:
218
+ try:
219
+ return response.json()
220
+ except json.JSONDecodeError:
221
+ return {"response": response.text}
222
+
223
+ elif response.status_code == 401:
224
+ raise AuthenticationError("Unauthorized - invalid or expired token")
225
+
226
+ elif response.status_code == 403:
227
+ try:
228
+ error_data = response.json()
229
+ raise InsufficientScopeError(error_data.get("error", "Insufficient permissions"))
230
+ except:
231
+ raise InsufficientScopeError("Insufficient permissions")
232
+
233
+ elif response.status_code == 429:
234
+ retry_after = response.headers.get("Retry-After")
235
+ raise RateLimitError(
236
+ "Rate limit exceeded",
237
+ retry_after=int(retry_after) if retry_after else None
238
+ )
239
+
240
+ else:
241
+ try:
242
+ error_data = response.json()
243
+ error_msg = error_data.get("error", error_data.get("message", "Unknown error"))
244
+ except:
245
+ error_msg = response.text or "Unknown error"
246
+
247
+ raise MC5APIError(
248
+ f"Request failed with status {response.status_code}: {error_msg}",
249
+ status_code=response.status_code,
250
+ response_data=error_data if 'error_data' in locals() else {}
251
+ )
252
+
253
+ except requests.exceptions.Timeout:
254
+ raise NetworkError("Request timed out")
255
+ except requests.exceptions.ConnectionError:
256
+ raise NetworkError("Connection failed")
257
+ except requests.exceptions.RequestException as e:
258
+ raise NetworkError(f"Network error: {str(e)}")
259
+
260
+ # Profile Management
261
+
262
+ def get_profile(self, credential: Optional[str] = None) -> Dict[str, Any]:
263
+ """
264
+ Get player profile information.
265
+
266
+ Args:
267
+ credential: Player credential (uses current user if not provided)
268
+
269
+ Returns:
270
+ Profile data
271
+ """
272
+ if credential:
273
+ url = f"{self.BASE_URLS['osiris']}/accounts/me"
274
+ params = {"credential": credential}
275
+ else:
276
+ url = f"{self.BASE_URLS['osiris']}/accounts/me"
277
+ params = {}
278
+
279
+ return self._make_request("GET", url, params=params)
280
+
281
+ def update_profile(self, profile_data: Dict[str, Any]) -> Dict[str, Any]:
282
+ """
283
+ Update player profile.
284
+
285
+ Args:
286
+ profile_data: Profile data to update
287
+
288
+ Returns:
289
+ Updated profile data
290
+ """
291
+ url = f"{self.BASE_URLS['osiris']}/accounts/me"
292
+ return self._make_request("PUT", url, data=profile_data)
293
+
294
+ # Clan Management
295
+
296
+ def get_clan_settings(self, clan_id: str) -> Dict[str, Any]:
297
+ """
298
+ Get clan settings and information.
299
+
300
+ Args:
301
+ clan_id: Clan ID
302
+
303
+ Returns:
304
+ Clan settings data including name, description, members, etc.
305
+ """
306
+ url = f"{self.BASE_URLS['osiris']}/clans/{clan_id}"
307
+ return self._make_request("GET", url)
308
+
309
+ def update_clan_settings(self, clan_id: str, settings: Dict[str, Any]) -> Dict[str, Any]:
310
+ """
311
+ Update clan settings.
312
+
313
+ Args:
314
+ clan_id: Clan ID
315
+ settings: Clan settings to update (name, description, membership_type, etc.)
316
+
317
+ Returns:
318
+ Updated clan settings
319
+ """
320
+ url = f"{self.BASE_URLS['osiris']}/clans/{clan_id}"
321
+ return self._make_request("PUT", url, data=settings)
322
+
323
+ def get_clan_members(self, clan_id: str) -> List[Dict[str, Any]]:
324
+ """
325
+ Get all members of a clan.
326
+
327
+ Args:
328
+ clan_id: Clan ID
329
+
330
+ Returns:
331
+ List of clan members with their roles and stats
332
+ """
333
+ url = f"{self.BASE_URLS['osiris']}/clans/{clan_id}/members"
334
+ response = self._make_request("GET", url)
335
+ return response.get("members", [])
336
+
337
+ def invite_clan_member(self, clan_id: str, credential: str, role: str = "member") -> Dict[str, Any]:
338
+ """
339
+ Invite a player to join the clan.
340
+
341
+ Args:
342
+ clan_id: Clan ID
343
+ credential: Player credential to invite
344
+ role: Role to assign (member, officer, etc.)
345
+
346
+ Returns:
347
+ Invitation result
348
+ """
349
+ url = f"{self.BASE_URLS['osiris']}/clans/{clan_id}/invitations"
350
+ data = {
351
+ "credential": credential,
352
+ "role": role
353
+ }
354
+ return self._make_request("POST", url, data=data)
355
+
356
+ def accept_clan_invitation(self, clan_id: str) -> Dict[str, Any]:
357
+ """
358
+ Accept a clan invitation.
359
+
360
+ Args:
361
+ clan_id: Clan ID
362
+
363
+ Returns:
364
+ Acceptance result
365
+ """
366
+ url = f"{self.BASE_URLS['osiris']}/clans/{clan_id}/invitations/me"
367
+ return self._make_request("POST", url)
368
+
369
+ def decline_clan_invitation(self, clan_id: str) -> Dict[str, Any]:
370
+ """
371
+ Decline a clan invitation.
372
+
373
+ Args:
374
+ clan_id: Clan ID
375
+
376
+ Returns:
377
+ Decline result
378
+ """
379
+ url = f"{self.BASE_URLS['osiris']}/clans/{clan_id}/invitations/me"
380
+ return self._make_request("DELETE", url)
381
+
382
+ def kick_clan_member(self, clan_id: str, member_credential: str) -> Dict[str, Any]:
383
+ """
384
+ Kick a member from the clan.
385
+
386
+ Args:
387
+ clan_id: Clan ID
388
+ member_credential: Credential of member to kick
389
+
390
+ Returns:
391
+ Operation result
392
+ """
393
+ url = f"{self.BASE_URLS['osiris']}/clans/{clan_id}/members/{member_credential}"
394
+ return self._make_request("DELETE", url)
395
+
396
+ def promote_clan_member(self, clan_id: str, member_credential: str, new_role: str) -> Dict[str, Any]:
397
+ """
398
+ Promote a clan member to a new role.
399
+
400
+ Args:
401
+ clan_id: Clan ID
402
+ member_credential: Credential of member to promote
403
+ new_role: New role (officer, leader, etc.)
404
+
405
+ Returns:
406
+ Promotion result
407
+ """
408
+ url = f"{self.BASE_URLS['osiris']}/clans/{clan_id}/members/{member_credential}/role"
409
+ data = {"role": new_role}
410
+ return self._make_request("PUT", url, data=data)
411
+
412
+ def demote_clan_member(self, clan_id: str, member_credential: str, new_role: str) -> Dict[str, Any]:
413
+ """
414
+ Demote a clan member to a new role.
415
+
416
+ Args:
417
+ clan_id: Clan ID
418
+ member_credential: Credential of member to demote
419
+ new_role: New role (member, etc.)
420
+
421
+ Returns:
422
+ Demotion result
423
+ """
424
+ url = f"{self.BASE_URLS['osiris']}/clans/{clan_id}/members/{member_credential}/role"
425
+ data = {"role": new_role}
426
+ return self._make_request("PUT", url, data=data)
427
+
428
+ def leave_clan(self, clan_id: str) -> Dict[str, Any]:
429
+ """
430
+ Leave the current clan.
431
+
432
+ Args:
433
+ clan_id: Clan ID
434
+
435
+ Returns:
436
+ Leave result
437
+ """
438
+ url = f"{self.BASE_URLS['osiris']}/clans/{clan_id}/members/me"
439
+ return self._make_request("DELETE", url)
440
+
441
+ def get_clan_applications(self, clan_id: str) -> List[Dict[str, Any]]:
442
+ """
443
+ Get pending clan applications.
444
+
445
+ Args:
446
+ clan_id: Clan ID
447
+
448
+ Returns:
449
+ List of pending applications
450
+ """
451
+ url = f"{self.BASE_URLS['osiris']}/clans/{clan_id}/applications"
452
+ response = self._make_request("GET", url)
453
+ return response.get("applications", [])
454
+
455
+ def accept_clan_application(self, clan_id: str, applicant_credential: str, role: str = "member") -> Dict[str, Any]:
456
+ """
457
+ Accept a clan application.
458
+
459
+ Args:
460
+ clan_id: Clan ID
461
+ applicant_credential: Credential of applicant
462
+ role: Role to assign (member, officer, etc.)
463
+
464
+ Returns:
465
+ Acceptance result
466
+ """
467
+ url = f"{self.BASE_URLS['osiris']}/clans/{clan_id}/applications/{applicant_credential}"
468
+ data = {"role": role}
469
+ return self._make_request("POST", url, data=data)
470
+
471
+ def reject_clan_application(self, clan_id: str, applicant_credential: str) -> Dict[str, Any]:
472
+ """
473
+ Reject a clan application.
474
+
475
+ Args:
476
+ clan_id: Clan ID
477
+ applicant_credential: Credential of applicant
478
+
479
+ Returns:
480
+ Rejection result
481
+ """
482
+ url = f"{self.BASE_URLS['osiris']}/clans/{clan_id}/applications/{applicant_credential}"
483
+ return self._make_request("DELETE", url)
484
+
485
+ def apply_to_clan(self, clan_id: str, message: str = "") -> Dict[str, Any]:
486
+ """
487
+ Apply to join a clan.
488
+
489
+ Args:
490
+ clan_id: Clan ID
491
+ message: Application message
492
+
493
+ Returns:
494
+ Application result
495
+ """
496
+ url = f"{self.BASE_URLS['osiris']}/clans/{clan_id}/applications"
497
+ data = {"message": message}
498
+ return self._make_request("POST", url, data=data)
499
+
500
+ def get_my_clan_applications(self) -> List[Dict[str, Any]]:
501
+ """
502
+ Get your own clan applications.
503
+
504
+ Returns:
505
+ List of your applications
506
+ """
507
+ url = f"{self.BASE_URLS['osiris']}/accounts/me/clan-applications"
508
+ response = self._make_request("GET", url)
509
+ return response.get("applications", [])
510
+
511
+ def cancel_clan_application(self, clan_id: str) -> Dict[str, Any]:
512
+ """
513
+ Cancel a clan application.
514
+
515
+ Args:
516
+ clan_id: Clan ID
517
+
518
+ Returns:
519
+ Cancellation result
520
+ """
521
+ url = f"{self.BASE_URLS['osiris']}/accounts/me/clan-applications/{clan_id}"
522
+ return self._make_request("DELETE", url)
523
+
524
+ def get_clan_statistics(self, clan_id: str) -> Dict[str, Any]:
525
+ """
526
+ Get clan statistics and performance data.
527
+
528
+ Args:
529
+ clan_id: Clan ID
530
+
531
+ Returns:
532
+ Clan statistics
533
+ """
534
+ url = f"{self.BASE_URLS['osiris']}/clans/{clan_id}/statistics"
535
+ return self._make_request("GET", url)
536
+
537
+ def get_clan_leaderboard(self, clan_id: str) -> List[Dict[str, Any]]:
538
+ """
539
+ Get clan internal leaderboard.
540
+
541
+ Args:
542
+ clan_id: Clan ID
543
+
544
+ Returns:
545
+ Clan leaderboard data
546
+ """
547
+ url = f"{self.BASE_URLS['osiris']}/clans/{clan_id}/leaderboard"
548
+ response = self._make_request("GET", url)
549
+ return response.get("leaderboard", [])
550
+
551
+ def search_clans(self, query: str, limit: int = 20) -> List[Dict[str, Any]]:
552
+ """
553
+ Search for clans.
554
+
555
+ Args:
556
+ query: Search query
557
+ limit: Maximum number of results
558
+
559
+ Returns:
560
+ List of matching clans
561
+ """
562
+ url = f"{self.BASE_URLS['osiris']}/clans/search"
563
+ params = {
564
+ "q": query,
565
+ "limit": limit
566
+ }
567
+ response = self._make_request("GET", url, params=params)
568
+ return response.get("clans", [])
569
+
570
+ def create_clan(self, name: str, tag: str, description: str = "", membership_type: str = "open") -> Dict[str, Any]:
571
+ """
572
+ Create a new clan.
573
+
574
+ Args:
575
+ name: Clan name
576
+ tag: Clan tag (short identifier)
577
+ description: Clan description
578
+ membership_type: Membership type (open, closed, invite_only)
579
+
580
+ Returns:
581
+ Created clan data
582
+ """
583
+ url = f"{self.BASE_URLS['osiris']}/clans"
584
+ data = {
585
+ "name": name,
586
+ "tag": tag,
587
+ "description": description,
588
+ "membership_type": membership_type
589
+ }
590
+ return self._make_request("POST", url, data=data)
591
+
592
+ def disband_clan(self, clan_id: str) -> Dict[str, Any]:
593
+ """
594
+ Disband a clan (leader only).
595
+
596
+ Args:
597
+ clan_id: Clan ID
598
+
599
+ Returns:
600
+ Disband result
601
+ """
602
+ url = f"{self.BASE_URLS['osiris']}/clans/{clan_id}"
603
+ return self._make_request("DELETE", url)
604
+
605
+ def transfer_clan_ownership(self, clan_id: str, new_leader_credential: str) -> Dict[str, Any]:
606
+ """
607
+ Transfer clan ownership to another member.
608
+
609
+ Args:
610
+ clan_id: Clan ID
611
+ new_leader_credential: Credential of new leader
612
+
613
+ Returns:
614
+ Transfer result
615
+ """
616
+ url = f"{self.BASE_URLS['osiris']}/clans/{clan_id}/ownership"
617
+ data = {"new_leader": new_leader_credential}
618
+ return self._make_request("POST", url, data=data)
619
+
620
+ # Group/Squad Management
621
+
622
+ def get_group_members(self, group_id: str, offset: int = 0) -> List[Dict[str, Any]]:
623
+ """
624
+ Get all members of a group/squad.
625
+
626
+ Args:
627
+ group_id: Group/Squad ID
628
+ offset: Pagination offset (default: 0)
629
+
630
+ Returns:
631
+ List of group members with their stats and status
632
+ """
633
+ url = f"{self.BASE_URLS['osiris']}/groups/{group_id}/members"
634
+ params = {
635
+ "group_id": group_id,
636
+ "offset": offset
637
+ }
638
+ response = self._make_request("GET", url, params=params)
639
+ return response if isinstance(response, list) else []
640
+
641
+ def update_group_member_stats(self, group_id: str, credential: str, **stats) -> Dict[str, Any]:
642
+ """
643
+ Update a group member's stats (score, XP, kill signature, etc.).
644
+
645
+ Args:
646
+ group_id: Group/Squad ID
647
+ credential: Member credential to update
648
+ **stats: Stats to update (_score, _xp, _killsig_id, _killsig_color, etc.)
649
+
650
+ Returns:
651
+ Update result
652
+ """
653
+ url = f"{self.BASE_URLS['osiris']}/groups/{group_id}/members/{credential}"
654
+
655
+ # Prepare data for form-encoded request
656
+ data = {
657
+ "operation": "update",
658
+ "credential": credential
659
+ }
660
+ data.update(stats)
661
+
662
+ return self._make_request("POST", url, data=data)
663
+
664
+ def update_member_score(self, group_id: str, credential: str, score: int) -> Dict[str, Any]:
665
+ """
666
+ Update a member's score in the group.
667
+
668
+ Args:
669
+ group_id: Group/Squad ID
670
+ credential: Member credential
671
+ score: New score value
672
+
673
+ Returns:
674
+ Update result
675
+ """
676
+ return self.update_group_member_stats(group_id, credential, _score=str(score))
677
+
678
+ def update_member_xp(self, group_id: str, credential: str, xp: int) -> Dict[str, Any]:
679
+ """
680
+ Update a member's XP in the group.
681
+
682
+ Args:
683
+ group_id: Group/Squad ID
684
+ credential: Member credential
685
+ xp: New XP value
686
+
687
+ Returns:
688
+ Update result
689
+ """
690
+ return self.update_group_member_stats(group_id, credential, _xp=str(xp))
691
+
692
+ def update_member_killsig(self, group_id: str, credential: str, killsig_id: str, killsig_color: str) -> Dict[str, Any]:
693
+ """
694
+ Update a member's kill signature.
695
+
696
+ Args:
697
+ group_id: Group/Squad ID
698
+ credential: Member credential
699
+ killsig_id: Kill signature ID
700
+ killsig_color: Kill signature color
701
+
702
+ Returns:
703
+ Update result
704
+ """
705
+ return self.update_group_member_stats(
706
+ group_id,
707
+ credential,
708
+ _killsig_id=killsig_id,
709
+ _killsig_color=killsig_color
710
+ )
711
+
712
+ def get_member_by_credential(self, group_id: str, credential: str) -> Optional[Dict[str, Any]]:
713
+ """
714
+ Get a specific member's information by credential.
715
+
716
+ Args:
717
+ group_id: Group/Squad ID
718
+ credential: Member credential
719
+
720
+ Returns:
721
+ Member information or None if not found
722
+ """
723
+ members = self.get_group_members(group_id)
724
+ for member in members:
725
+ if member.get("credential") == credential:
726
+ return member
727
+ return None
728
+
729
+ def get_online_members(self, group_id: str) -> List[Dict[str, Any]]:
730
+ """
731
+ Get all online members in a group.
732
+
733
+ Args:
734
+ group_id: Group/Squad ID
735
+
736
+ Returns:
737
+ List of online members
738
+ """
739
+ members = self.get_group_members(group_id)
740
+ return [member for member in members if member.get("online", False)]
741
+
742
+ def get_group_statistics(self, group_id: str) -> Dict[str, Any]:
743
+ """
744
+ Calculate group statistics from member data.
745
+
746
+ Args:
747
+ group_id: Group/Squad ID
748
+
749
+ Returns:
750
+ Group statistics (total members, online count, average score, etc.)
751
+ """
752
+ members = self.get_group_members(group_id)
753
+
754
+ if not members:
755
+ return {
756
+ "total_members": 0,
757
+ "online_members": 0,
758
+ "offline_members": 0,
759
+ "average_score": 0,
760
+ "total_score": 0,
761
+ "average_xp": 0,
762
+ "total_xp": 0
763
+ }
764
+
765
+ online_count = sum(1 for member in members if member.get("online", False))
766
+ scores = [int(member.get("_score", 0)) for member in members if member.get("_score")]
767
+ xp_values = [int(member.get("_xp", 0)) for member in members if member.get("_xp")]
768
+
769
+ return {
770
+ "total_members": len(members),
771
+ "online_members": online_count,
772
+ "offline_members": len(members) - online_count,
773
+ "average_score": sum(scores) / len(scores) if scores else 0,
774
+ "total_score": sum(scores),
775
+ "average_xp": sum(xp_values) / len(xp_values) if xp_values else 0,
776
+ "total_xp": sum(xp_values)
777
+ }
778
+
779
+ def send_squad_wall_message(self, clan_id: str, message: str, msg_type: int = 0,
780
+ player_killsig: str = None, player_killsig_color: str = None,
781
+ language: str = "en", activity_type: str = "user_post") -> Dict[str, Any]:
782
+ """
783
+ Send a message to the squad/group wall.
784
+
785
+ Args:
786
+ clan_id: Group/Squad ID
787
+ message: Message content
788
+ msg_type: Message type (0 for regular message)
789
+ player_killsig: Player kill signature ID (optional)
790
+ player_killsig_color: Player kill signature color (optional)
791
+ language: Message language (default: "en")
792
+ activity_type: Activity type (default: "user_post")
793
+
794
+ Returns:
795
+ Message post result with message ID
796
+ """
797
+ url = f"{self.BASE_URLS['osiris']}/groups/{clan_id}/wall"
798
+
799
+ # Create message data
800
+ message_data = {
801
+ "msg_body": message,
802
+ "msg_type": msg_type
803
+ }
804
+
805
+ # Add kill signature if provided
806
+ if player_killsig:
807
+ message_data["playerKillSign"] = player_killsig
808
+ if player_killsig_color is not None:
809
+ message_data["playerKillSignColor"] = player_killsig_color
810
+
811
+ # Convert to JSON string as required by API
812
+ import json
813
+ text_json = json.dumps(message_data)
814
+
815
+ # Prepare form data
816
+ data = {
817
+ "text": text_json + "\n",
818
+ "language": language,
819
+ "activity_type": activity_type,
820
+ "alert_kairos": "false"
821
+ }
822
+
823
+ return self._make_request("POST", url, data=data)
824
+
825
+ def get_squad_wall_messages(self, clan_id: str, limit: int = 20) -> List[Dict[str, Any]]:
826
+ """
827
+ Get messages from the squad wall.
828
+
829
+ Args:
830
+ clan_id: Group/Squad ID
831
+ limit: Maximum number of messages to retrieve
832
+
833
+ Returns:
834
+ List of wall messages
835
+ """
836
+ url = f"{self.BASE_URLS['osiris']}/groups/{clan_id}/wall"
837
+ params = {
838
+ "limit": limit
839
+ }
840
+ response = self._make_request("GET", url, params=params)
841
+ return response if isinstance(response, list) else []
842
+
843
+ def delete_squad_wall_message(self, clan_id: str, message_id: str) -> Dict[str, Any]:
844
+ """
845
+ Delete a message from the squad wall.
846
+
847
+ Args:
848
+ clan_id: Group/Squad ID
849
+ message_id: Message ID to delete
850
+
851
+ Returns:
852
+ Deletion result
853
+ """
854
+ url = f"{self.BASE_URLS['osiris']}/groups/{clan_id}/wall/{message_id}"
855
+ return self._make_request("DELETE", url)
856
+
857
+ # Private Messaging
858
+
859
+ def send_private_message(self, credential: str, message: str, reply_to: str = None,
860
+ kill_sign_color: str = None, kill_sign_name: str = None,
861
+ message_type: str = "inbox", alert_kairos: bool = False) -> Dict[str, Any]:
862
+ """
863
+ Send a private message to a player.
864
+
865
+ Args:
866
+ credential: Recipient credential
867
+ message: Message content
868
+ reply_to: Message ID to reply to (optional)
869
+ kill_sign_color: Kill signature color (optional)
870
+ kill_sign_name: Kill signature name (optional)
871
+ message_type: Message type (default: "inbox")
872
+ alert_kairos: Whether to send alert notification (default: False)
873
+
874
+ Returns:
875
+ Message send result
876
+ """
877
+ url = f"{self.BASE_URLS['hermes']}/messages/inbox/{credential}"
878
+
879
+ # Create message data
880
+ message_data = {
881
+ "body": message,
882
+ "_type": message_type
883
+ }
884
+
885
+ # Add reply_to if provided
886
+ if reply_to:
887
+ message_data["reply_to"] = reply_to
888
+
889
+ # Add kill signature if provided
890
+ if kill_sign_color is not None:
891
+ message_data["_killSignColor"] = kill_sign_color
892
+ if kill_sign_name:
893
+ message_data["_killSignName"] = kill_sign_name
894
+
895
+ # Convert to JSON string as required by API
896
+ import json
897
+ body_json = json.dumps(message_data)
898
+
899
+ # Prepare form data
900
+ data = {
901
+ "from": "RxZ Saitama", # Replace with actual sender name
902
+ "body": body_json + "\n",
903
+ "alert_kairos": str(alert_kairos).lower()
904
+ }
905
+
906
+ return self._make_request("POST", url, data=data)
907
+
908
+ def get_inbox_messages(self, limit: int = 20) -> List[Dict[str, Any]]:
909
+ """
910
+ Get messages from your inbox.
911
+
912
+ Args:
913
+ limit: Maximum number of messages to retrieve
914
+
915
+ Returns:
916
+ List of inbox messages
917
+ """
918
+ url = f"{self.BASE_URLS['hermes']}/messages/inbox"
919
+ params = {
920
+ "limit": limit
921
+ }
922
+ response = self._make_request("GET", url, params=params)
923
+ return response if isinstance(response, list) else []
924
+
925
+ def delete_inbox_message(self, message_id: str) -> Dict[str, Any]:
926
+ """
927
+ Delete a message from your inbox.
928
+
929
+ Args:
930
+ message_id: Message ID to delete
931
+
932
+ Returns:
933
+ Deletion result
934
+ """
935
+ url = f"{self.BASE_URLS['hermes']}/messages/inbox/me"
936
+ params = {
937
+ "msgids": message_id
938
+ }
939
+ return self._make_request("DELETE", url, params=params)
940
+
941
+ def delete_multiple_inbox_messages(self, message_ids: List[str]) -> Dict[str, Any]:
942
+ """
943
+ Delete multiple messages from your inbox at once.
944
+
945
+ Args:
946
+ message_ids: List of message IDs to delete
947
+
948
+ Returns:
949
+ Deletion result
950
+ """
951
+ url = f"{self.BASE_URLS['hermes']}/messages/inbox/me"
952
+ params = {
953
+ "msgids": ",".join(message_ids)
954
+ }
955
+ return self._make_request("DELETE", url, params=params)
956
+
957
+ def clear_inbox(self) -> Dict[str, Any]:
958
+ """
959
+ Clear all messages from your inbox.
960
+
961
+ Returns:
962
+ Deletion result
963
+ """
964
+ # Get all messages first
965
+ messages = self.get_inbox_messages(limit=1000) # Get up to 1000 messages
966
+ if messages:
967
+ message_ids = [msg.get('id', '') for msg in messages if msg.get('id')]
968
+ return self.delete_multiple_inbox_messages(message_ids)
969
+ return {"status": "success", "message": "No messages to delete"}
970
+
971
+ def send_squad_wall_message(self, clan_id: str, message: str, msg_type: int = 0,
972
+ player_killsig: str = None, player_killsig_color: str = None,
973
+ language: str = "en", activity_type: str = "user_post") -> Dict[str, Any]:
974
+ """
975
+ Send a message to the squad/group wall.
976
+
977
+ Args:
978
+ clan_id: Group/Squad ID
979
+ message: Message content
980
+ msg_type: Message type (0 for regular message)
981
+ player_killsig: Player kill signature ID (optional)
982
+ player_killsig_color: Player kill signature color (optional)
983
+ language: Message language (default: "en")
984
+ activity_type: Activity type (default: "user_post")
985
+
986
+ Returns:
987
+ Message post result with message ID
988
+ """
989
+ url = f"{self.BASE_URLS['osiris']}/groups/{clan_id}/wall"
990
+
991
+ # Create message data
992
+ message_data = {
993
+ "msg_body": message,
994
+ "msg_type": msg_type
995
+ }
996
+
997
+ # Add kill signature if provided
998
+ if player_killsig:
999
+ message_data["playerKillSign"] = player_killsig
1000
+ if player_killsig_color is not None:
1001
+ message_data["playerKillSignColor"] = player_killsig_color
1002
+
1003
+ # Convert to JSON string as required by API
1004
+ import json
1005
+ text_json = json.dumps(message_data)
1006
+
1007
+ # Prepare form data
1008
+ data = {
1009
+ "text": text_json + "\n",
1010
+ "language": language,
1011
+ "activity_type": activity_type,
1012
+ "alert_kairos": "false"
1013
+ }
1014
+
1015
+ return self._make_request("POST", url, data=data)
1016
+
1017
+ # Friend Management
1018
+
1019
+ def get_friends(self) -> List[Dict[str, Any]]:
1020
+ """
1021
+ Get friends list.
1022
+
1023
+ Returns:
1024
+ List of friends
1025
+ """
1026
+ url = f"{self.BASE_URLS['osiris']}/accounts/me/friends"
1027
+ response = self._make_request("GET", url)
1028
+ return response.get("friends", [])
1029
+
1030
+ def send_friend_request(self, credential: str) -> Dict[str, Any]:
1031
+ """
1032
+ Send friend request.
1033
+
1034
+ Args:
1035
+ credential: Target player credential
1036
+
1037
+ Returns:
1038
+ Friend request result
1039
+ """
1040
+ url = f"{self.BASE_URLS['osiris']}/accounts/me/friends"
1041
+ data = {"credential": credential}
1042
+ return self._make_request("POST", url, data=data)
1043
+
1044
+ def check_friend_status(self, credential: str) -> Dict[str, Any]:
1045
+ """
1046
+ Check friend connection status.
1047
+
1048
+ Args:
1049
+ credential: Target player credential
1050
+
1051
+ Returns:
1052
+ Friend status information
1053
+ """
1054
+ url = f"{self.BASE_URLS['osiris']}/accounts/me/connections/friend/{credential}"
1055
+ return self._make_request("GET", url)
1056
+
1057
+ # Messaging
1058
+
1059
+ def send_squad_wall_message(self, clan_id: str, message: str) -> Dict[str, Any]:
1060
+ """
1061
+ Send message to squad wall.
1062
+
1063
+ Args:
1064
+ clan_id: Clan ID
1065
+ message: Message content
1066
+
1067
+ Returns:
1068
+ Message send result
1069
+ """
1070
+ url = f"{self.BASE_URLS['osiris']}/clans/{clan_id}/wall"
1071
+ data = {"message": message}
1072
+ return self._make_request("POST", url, data=data)
1073
+
1074
+ # Events
1075
+
1076
+ def get_events(self) -> List[Dict[str, Any]]:
1077
+ """
1078
+ Get list of active events.
1079
+
1080
+ Returns:
1081
+ List of events
1082
+ """
1083
+ url = f"{self.BASE_URLS['osiris']}/events"
1084
+ response = self._make_request("GET", url)
1085
+ return response if isinstance(response, list) else response.get("events", [])
1086
+
1087
+ def get_event_details(self, event_name: str) -> Dict[str, Any]:
1088
+ """
1089
+ Get details for a specific event.
1090
+
1091
+ Args:
1092
+ event_name: Event name
1093
+
1094
+ Returns:
1095
+ Event details
1096
+ """
1097
+ events = self.get_events()
1098
+ for event in events:
1099
+ if event.get("name") == event_name:
1100
+ return event
1101
+ raise MC5APIError(f"Event '{event_name}' not found")
1102
+
1103
+ # Leaderboard
1104
+
1105
+ def get_leaderboard(self, leaderboard_type: str = "ro") -> Dict[str, Any]:
1106
+ """
1107
+ Get leaderboard data.
1108
+
1109
+ Args:
1110
+ leaderboard_type: Leaderboard type (ro for read-only, admin for admin)
1111
+
1112
+ Returns:
1113
+ Leaderboard data
1114
+ """
1115
+ if leaderboard_type == "admin":
1116
+ url = f"{self.BASE_URLS['olympus']}/leaderboards/desc"
1117
+ else:
1118
+ url = f"{self.BASE_URLS['osiris']}/accounts/leaderboard_{leaderboard_type}"
1119
+
1120
+ return self._make_request("GET", url)
1121
+
1122
+ # Game Configuration
1123
+
1124
+ def get_game_object_catalog(self) -> List[Dict[str, Any]]:
1125
+ """
1126
+ Get game object catalog.
1127
+
1128
+ Returns:
1129
+ List of game objects
1130
+ """
1131
+ url = f"{self.BASE_URLS['iris']}/1875/game_object_{int(time.time())}"
1132
+ response = self._make_request("GET", url, require_token=False)
1133
+ return response if isinstance(response, list) else []
1134
+
1135
+ def get_asset_hash_metadata(self, asset_path: str) -> Dict[str, Any]:
1136
+ """
1137
+ Get asset hash metadata.
1138
+
1139
+ Args:
1140
+ asset_path: Asset path
1141
+
1142
+ Returns:
1143
+ Asset metadata
1144
+ """
1145
+ url = f"{self.BASE_URLS['iris']}/assets/{asset_path}/metadata/hash"
1146
+ return self._make_request("GET", url, require_token=False)
1147
+
1148
+ # Alias and Dogtags
1149
+
1150
+ def get_alias_info(self, alias_id: str) -> Dict[str, Any]:
1151
+ """
1152
+ Get alias information.
1153
+
1154
+ Args:
1155
+ alias_id: Player alias ID
1156
+
1157
+ Returns:
1158
+ Alias information
1159
+ """
1160
+ url = f"{self.BASE_URLS['auth']}/games/mygame/alias/{alias_id}"
1161
+ return self._make_request("GET", url)
1162
+
1163
+ def convert_dogtag_to_alias(self, dogtag: str) -> str:
1164
+ """
1165
+ Convert dogtag to alias.
1166
+
1167
+ Args:
1168
+ dogtag: Dogtag string
1169
+
1170
+ Returns:
1171
+ Alias string
1172
+ """
1173
+ alias = ""
1174
+ for char in dogtag.lower():
1175
+ if char.isdigit():
1176
+ # Digit conversion: (digit - 2) modulo 10
1177
+ alias_char = str((int(char) - 2) % 10)
1178
+ elif char.isalpha():
1179
+ # Letter conversion: subtract 2 from ASCII value
1180
+ alias_char = chr(ord(char) - 2)
1181
+ else:
1182
+ alias_char = char
1183
+ alias += alias_char
1184
+ return alias
1185
+
1186
+ # Utility Methods
1187
+
1188
+ def get_token_info(self) -> Dict[str, Any]:
1189
+ """
1190
+ Get current token information.
1191
+
1192
+ Returns:
1193
+ Token data
1194
+ """
1195
+ self._ensure_valid_token()
1196
+ return self._token_data.copy()
1197
+
1198
+ def is_authenticated(self) -> bool:
1199
+ """
1200
+ Check if client is authenticated with valid token.
1201
+
1202
+ Returns:
1203
+ True if authenticated, False otherwise
1204
+ """
1205
+ return self._token_data is not None and self.token_generator.validate_token(self._token_data)
1206
+
1207
+ def close(self):
1208
+ """Close the HTTP session and cleanup resources."""
1209
+ if self.session:
1210
+ self.session.close()
1211
+ if self.token_generator:
1212
+ self.token_generator.close()
1213
+
1214
+ def __enter__(self):
1215
+ """Context manager entry."""
1216
+ return self
1217
+
1218
+ def __exit__(self, exc_type, exc_val, exc_tb):
1219
+ """Context manager exit."""
1220
+ self.close()