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.
@@ -0,0 +1,682 @@
1
+ #!/usr/bin/env python3
2
+ # ────────────[ CHIZOBA ]────────────────────────────
3
+ # | Email : chizoba2026@hotmail.com
4
+ # | File : easy_mc5.py
5
+ # | License : MIT License © 2026 Chizoba
6
+ # | Brief | Easy-to-use MC5 functions for everyday tasks
7
+ # ────────────────★─────────────────────────────────
8
+
9
+ """
10
+ Easy MC5 - Simplified Interface for Everyday Tasks
11
+ ==================================================
12
+
13
+ This module provides simple, one-line functions for the most common MC5 tasks.
14
+ No complex setup, no confusing API calls - just simple functions that work.
15
+
16
+ Quick Start:
17
+ from mc5_api_client.easy_mc5 import MC5Easy
18
+
19
+ # Initialize with your credentials
20
+ mc5 = MC5Easy("your_credential", "your_password")
21
+
22
+ # Check your daily tasks
23
+ tasks = mc5.check_daily_tasks()
24
+
25
+ # Get your profile
26
+ profile = mc5.get_my_profile()
27
+
28
+ # Search for a player
29
+ player = mc5.find_player("f55f")
30
+
31
+ # Send a message
32
+ mc5.send_message_to_friend("f55f", "Hello!")
33
+ """
34
+
35
+ import os
36
+ from typing import Optional, Dict, Any, List
37
+ from .client import MC5Client, Platform
38
+ from .exceptions import MC5APIError
39
+
40
+
41
+ class MC5Easy:
42
+ """
43
+ Easy-to-use MC5 client for everyday tasks.
44
+
45
+ Simplifies common MC5 operations into simple, one-line functions.
46
+ """
47
+
48
+ def __init__(self, username: str = None, password: str = None, platform: Platform = Platform.PC):
49
+ """
50
+ Initialize MC5Easy client.
51
+
52
+ Args:
53
+ username: MC5 credential (can be set via environment variable MC5_USERNAME)
54
+ password: MC5 password (can be set via environment variable MC5_PASSWORD)
55
+ platform: Platform to use (PC or Android)
56
+ """
57
+ self.username = username or os.getenv('MC5_USERNAME')
58
+ self.password = password or os.getenv('MC5_PASSWORD')
59
+ self.platform = platform
60
+ self.client = None
61
+ self._connected = False
62
+
63
+ if not self.username or not self.password:
64
+ raise ValueError("Username and password are required. Set them directly or use MC5_USERNAME and MC5_PASSWORD environment variables.")
65
+
66
+ def connect(self) -> bool:
67
+ """Connect to MC5 servers."""
68
+ try:
69
+ self.client = MC5Client(
70
+ username=self.username,
71
+ password=self.password,
72
+ platform=self.platform
73
+ )
74
+ self.client.authenticate()
75
+ self._connected = True
76
+ return True
77
+ except Exception as e:
78
+ print(f"❌ Connection failed: {e}")
79
+ self._connected = False
80
+ return False
81
+
82
+ def is_connected(self) -> bool:
83
+ """Check if connected to MC5."""
84
+ return self._connected and self.client is not None
85
+
86
+ def _ensure_connected(self):
87
+ """Ensure we're connected to MC5."""
88
+ if not self.is_connected():
89
+ if not self.connect():
90
+ raise ConnectionError("Not connected to MC5. Call connect() first.")
91
+
92
+ # === PROFILE FUNCTIONS ===
93
+
94
+ def get_my_profile(self) -> Dict[str, Any]:
95
+ """Get your profile information."""
96
+ self._ensure_connected()
97
+ try:
98
+ profile = self.client.get_profile()
99
+ return profile
100
+ except Exception as e:
101
+ print(f"❌ Error getting profile: {e}")
102
+ return {}
103
+
104
+ def get_my_stats(self) -> Dict[str, Any]:
105
+ """Get your stats in a simple format."""
106
+ profile = self.get_my_profile()
107
+ if not profile:
108
+ return {}
109
+
110
+ return {
111
+ 'name': profile.get('name', 'Unknown'),
112
+ 'level': profile.get('level') or profile.get('_level') or profile.get('player_level') or 'Unknown',
113
+ 'xp': profile.get('xp') or profile.get('_xp') or profile.get('experience') or 0,
114
+ 'score': profile.get('score') or profile.get('_score') or profile.get('player_score') or 0,
115
+ 'clan_id': profile.get('groups', [None])[0]
116
+ }
117
+
118
+ # === PLAYER SEARCH FUNCTIONS ===
119
+
120
+ def find_player(self, dogtag: str) -> Dict[str, Any]:
121
+ """Find a player by dogtag."""
122
+ self._ensure_connected()
123
+ try:
124
+ player = self.client.search_player(dogtag)
125
+ if player and 'kills' in player:
126
+ return {
127
+ 'dogtag': dogtag,
128
+ 'found': True,
129
+ 'kills': player.get('kills', 0),
130
+ 'level': player.get('level') or player.get('_level') or player.get('player_level') or 'Unknown',
131
+ 'score': player.get('score') or player.get('_score') or player.get('player_score') or 0,
132
+ 'wins': player.get('wins', 0),
133
+ 'losses': player.get('losses', 0),
134
+ 'draws': player.get('draws', 0)
135
+ }
136
+ else:
137
+ return {'dogtag': dogtag, 'found': False}
138
+ except Exception as e:
139
+ print(f"❌ Error finding player {dogtag}: {e}")
140
+ return {'dogtag': dogtag, 'found': False, 'error': str(e)}
141
+
142
+ def compare_players(self, dogtag1: str, dogtag2: str) -> Dict[str, Any]:
143
+ """Compare two players."""
144
+ player1 = self.find_player(dogtag1)
145
+ player2 = self.find_player(dogtag2)
146
+
147
+ return {
148
+ 'player1': player1,
149
+ 'player2': player2,
150
+ 'comparison': {
151
+ 'player1_wins_kills': player1.get('kills', 0) > player2.get('kills', 0),
152
+ 'player1_wins_score': player1.get('score', 0) > player2.get('score', 0),
153
+ 'player1_wins_level': player1.get('level', 0) > player2.get('level', 0)
154
+ }
155
+ }
156
+
157
+ # === CLAN/SQUAD FUNCTIONS ===
158
+
159
+ def get_my_clan_members(self) -> List[Dict[str, Any]]:
160
+ """Get your clan members."""
161
+ self._ensure_connected()
162
+ try:
163
+ # First get profile to find clan ID
164
+ profile = self.get_my_profile()
165
+ clan_id = profile.get('groups', [None])[0]
166
+
167
+ if not clan_id:
168
+ return []
169
+
170
+ members = self.client.get_group_members(clan_id)
171
+ return members
172
+ except Exception as e:
173
+ print(f"❌ Error getting clan members: {e}")
174
+ return []
175
+
176
+ def get_clan_stats(self) -> Dict[str, Any]:
177
+ """Get clan statistics."""
178
+ members = self.get_my_clan_members()
179
+ if not members:
180
+ return {}
181
+
182
+ online_count = len([m for m in members if m.get('online', False)])
183
+ total_score = sum(int(m.get('_score') or m.get('score') or 0) for m in members)
184
+ avg_score = total_score // len(members) if members else 0
185
+
186
+ # Sort by score
187
+ sorted_members = sorted(members, key=lambda x: int(x.get('_score') or x.get('score') or 0), reverse=True)
188
+
189
+ return {
190
+ 'total_members': len(members),
191
+ 'online_members': online_count,
192
+ 'total_score': total_score,
193
+ 'average_score': avg_score,
194
+ 'top_member': sorted_members[0] if sorted_members else None,
195
+ 'top_5_members': sorted_members[:5]
196
+ }
197
+
198
+ # === DAILY TASKS FUNCTIONS ===
199
+
200
+ def check_daily_tasks(self) -> List[Dict[str, Any]]:
201
+ """Check your daily tasks."""
202
+ self._ensure_connected()
203
+ try:
204
+ events = self.client.get_events()
205
+ daily_tasks = [e for e in events if 'daily' in e.get('name', '').lower() or 'activities' in e.get('name', '').lower()]
206
+ return daily_tasks
207
+ except Exception as e:
208
+ print(f"❌ Error checking daily tasks: {e}")
209
+ return []
210
+
211
+ def get_daily_summary(self) -> Dict[str, Any]:
212
+ """Get a summary of your daily status."""
213
+ tasks = self.check_daily_tasks()
214
+ profile = self.get_my_stats()
215
+ clan_stats = self.get_clan_stats()
216
+
217
+ return {
218
+ 'daily_tasks': {
219
+ 'count': len(tasks),
220
+ 'tasks': tasks
221
+ },
222
+ 'profile': profile,
223
+ 'clan': clan_stats
224
+ }
225
+
226
+ # === MESSAGING FUNCTIONS ===
227
+
228
+ def send_message_to_friend(self, dogtag: str, message: str) -> bool:
229
+ """Send a message to a friend."""
230
+ self._ensure_connected()
231
+ try:
232
+ result = self.client.send_private_message(dogtag, message)
233
+ return result is not None
234
+ except Exception as e:
235
+ print(f"❌ Error sending message to {dogtag}: {e}")
236
+ return False
237
+
238
+ def broadcast_to_clan(self, message: str) -> bool:
239
+ """Broadcast a message to your clan wall."""
240
+ self._ensure_connected()
241
+ try:
242
+ profile = self.get_my_profile()
243
+ clan_id = profile.get('groups', [None])[0]
244
+
245
+ if not clan_id:
246
+ print("❌ Not in a clan")
247
+ return False
248
+
249
+ result = self.client.post_group_wall(clan_id, message)
250
+ return result is not None
251
+ except Exception as e:
252
+ print(f"❌ Error broadcasting to clan: {e}")
253
+ return False
254
+
255
+ # === UTILITY FUNCTIONS ===
256
+
257
+ def quick_status(self) -> str:
258
+ """Get a quick status overview."""
259
+ try:
260
+ profile = self.get_my_stats()
261
+ tasks = self.check_daily_tasks()
262
+ clan_stats = self.get_clan_stats()
263
+
264
+ status = f"🎮 MC5 Status:\n"
265
+ status += f"👤 Player: {profile.get('name', 'Unknown')} (Level {profile.get('level', 'Unknown')})\n"
266
+ status += f"⭐ XP: {profile.get('xp', 0):,}\n"
267
+ status += f"💰 Score: {profile.get('score', 0):,}\n"
268
+ status += f"📅 Daily Tasks: {len(tasks)} pending\n"
269
+
270
+ if clan_stats.get('total_members', 0) > 0:
271
+ status += f"🏰 Clan: {clan_stats['total_members']} members ({clan_stats['online_members']} online)\n"
272
+
273
+ return status
274
+ except Exception as e:
275
+ return f"❌ Error getting status: {e}"
276
+
277
+ def run_daily_routine(self) -> Dict[str, Any]:
278
+ """Run a complete daily routine."""
279
+ print("🚀 Running daily MC5 routine...")
280
+
281
+ results = {
282
+ 'profile': self.get_my_stats(),
283
+ 'daily_tasks': self.check_daily_tasks(),
284
+ 'clan_stats': self.get_clan_stats(),
285
+ 'status': self.quick_status()
286
+ }
287
+
288
+ print("✅ Daily routine completed!")
289
+ return results
290
+
291
+ def close(self):
292
+ """Close the connection."""
293
+ if self.client:
294
+ self.client.close()
295
+ self._connected = False
296
+
297
+ def __enter__(self):
298
+ """Context manager entry."""
299
+ return self
300
+
301
+ def __exit__(self, exc_type, exc_val, exc_tb):
302
+ """Context manager exit."""
303
+ self.close()
304
+
305
+ # === DEVICE ID FUNCTIONS ===
306
+
307
+ def get_global_id(self, device_id: str = None, device_type: str = "w10", hdidfv: str = None) -> str:
308
+ """
309
+ Get a global device ID from Gameloft's global ID service.
310
+
311
+ Args:
312
+ device_id: Your device ID (optional, will generate one if not provided)
313
+ device_type: Device type (w10, android, ios, etc.)
314
+ hdidfv: Hardware ID fingerprint (optional)
315
+
316
+ Returns:
317
+ Global device ID string
318
+ """
319
+ self._ensure_connected()
320
+
321
+ try:
322
+ # Build request parameters
323
+ params = {
324
+ "source": "Identifiers_6.0.0",
325
+ "client_id": "1875:55979:6.0.0a:windows:windows",
326
+ "device_type": device_type,
327
+ "global_device_id": device_id or "1364509832654538259",
328
+ "hdidfv": hdidfv or "76dfc72e-7850-4d9e-b79c-9861c7e3ea20"
329
+ }
330
+
331
+ headers = {
332
+ "Accept": "*/*",
333
+ "Accept-Encoding": "gzip;q=1.0, deflate;q=1.0, identity;q=0.5, *;q=0"
334
+ }
335
+
336
+ # Make request
337
+ response = self.client.session.get(
338
+ "https://gdid.datalake.gameloft.com/assign_global_id/",
339
+ params=params,
340
+ headers=headers,
341
+ timeout=30
342
+ )
343
+
344
+ if response.status_code == 200:
345
+ return response.text.strip()
346
+ else:
347
+ print(f"❌ Global ID request failed: {response.status_code}")
348
+ return ""
349
+
350
+ except Exception as e:
351
+ print(f"❌ Error getting global ID: {e}")
352
+ return ""
353
+
354
+ def get_device_info(self) -> Dict[str, Any]:
355
+ """
356
+ Get comprehensive device information including global ID.
357
+
358
+ Returns:
359
+ Dictionary with device information
360
+ """
361
+ self._ensure_connected()
362
+
363
+ try:
364
+ # Get global ID
365
+ global_id = self.get_global_id()
366
+
367
+ # Get device info from profile
368
+ profile = self.get_my_stats()
369
+
370
+ return {
371
+ "global_id": global_id,
372
+ "device_type": "w10", # Default for PC
373
+ "client_id": "1875:55979:6.0.0a:windows:windows",
374
+ "profile_name": profile.get('name', 'Unknown'),
375
+ "profile_level": profile.get('level', 'Unknown'),
376
+ "profile_score": profile.get('score', 0),
377
+ "has_clan": bool(profile.get('clan_id')),
378
+ "timestamp": "Unknown"
379
+ }
380
+
381
+ except Exception as e:
382
+ print(f"❌ Error getting device info: {e}")
383
+ return {"error": str(e)}
384
+
385
+ def generate_device_id(self) -> str:
386
+ """
387
+ Generate a unique device ID for the current device.
388
+
389
+ Returns:
390
+ Generated device ID string
391
+ """
392
+ import uuid
393
+ import random
394
+ import string
395
+
396
+ # Generate a unique device ID
397
+ device_id = f"mc5_{uuid.uuid4().hex[:16]}"
398
+ return device_id
399
+
400
+ # === FEDERATION SESSION FUNCTIONS ===
401
+
402
+ def create_federation_session(self, device_id: str = None) -> Dict[str, Any]:
403
+ """
404
+ Create a federation session for MC5 game launch.
405
+
406
+ Args:
407
+ device_id: Device ID for the session (optional)
408
+
409
+ Returns:
410
+ Dictionary with session information
411
+ """
412
+ self._ensure_connected()
413
+
414
+ try:
415
+ # Build request parameters
416
+ params = {
417
+ "password": self.password,
418
+ "device_id": device_id or "1364509832654538259"
419
+ }
420
+
421
+ headers = {
422
+ "Accept": "*/*",
423
+ "Content-Type": "application/x-www-form-urlencoded"
424
+ }
425
+
426
+ # Build URL with encoded credential
427
+ encoded_credential = self.username.replace(":", "%3A")
428
+ url = f"https://federation-eur.gameloft.com/sessions/1875%3A55979%3A6.0.0a%3Awindows%3Awindows/{encoded_credential}"
429
+
430
+ # Make request
431
+ response = self.client.session.post(
432
+ url,
433
+ data=params,
434
+ headers=headers,
435
+ timeout=30
436
+ )
437
+
438
+ if response.status_code == 200:
439
+ return {
440
+ "status": "OK",
441
+ "response": response.text.strip(),
442
+ "service_type": service_type,
443
+ "timestamp": params["timestamp"],
444
+ "domain": params["domain"]
445
+ }
446
+ else:
447
+ return {
448
+ "status": "Failed",
449
+ "error": f"HTTP {response.status_code}",
450
+ "response": response.text,
451
+ "service_type": service_type
452
+ }
453
+
454
+ except Exception as e:
455
+ print(f"❌ Error checking connection status: {e}")
456
+ return {"error": str(e), "service_type": service_type}
457
+
458
+ def get_connection_status_all(self) -> Dict[str, Any]:
459
+ """
460
+ Check connection status for all services.
461
+
462
+ Returns:
463
+ Dictionary with all service connection statuses
464
+ """
465
+ services = ["auth", "matchmaking", "lobby", "gs", "sp"]
466
+
467
+ connection_statuses = {}
468
+
469
+ for service in services:
470
+ status = self.check_connection_status(service)
471
+ connection_statuses[service] = status
472
+
473
+ return connection_statuses
474
+
475
+ # === ROOM FINDER FUNCTIONS ===
476
+
477
+ def find_rooms(self, clan_id: str = None, clan_battle_room: bool = False, server_type: str = "mp_server") -> List[Dict[str, Any]]:
478
+ """
479
+ Find available rooms using AnubisFinder.
480
+
481
+ Args:
482
+ clan_id: Clan ID to filter rooms (optional)
483
+ clan_battle_room: Filter for clan battle rooms (optional)
484
+ server_type: Server type (mp_server)
485
+
486
+ Returns:
487
+ List of room dictionaries
488
+ """
489
+ self._ensure_connected()
490
+
491
+ try:
492
+ # Build request parameters
493
+ params = {
494
+ "_can_spectate": "false",
495
+ "_customs_room": "true",
496
+ "_event_id": "x",
497
+ "_joinable": "true",
498
+ "_public": "true",
499
+ "full": "false",
500
+ "game_started": "true",
501
+ "server_type": server_type
502
+ }
503
+
504
+ # Add clan filters if provided
505
+ if clan_id:
506
+ params["_clan_" + clan_id] = "true"
507
+
508
+ if clan_battle_room:
509
+ params["_clan_battle_room"] = "true"
510
+
511
+ headers = {
512
+ "Accept": "*/*"
513
+ }
514
+
515
+ # Make request
516
+ response = self.client.session.get(
517
+ "https://eur-anubisfinder.gameloft.com/rooms/1875:55979:6.0.0a:windows:windows",
518
+ params=params,
519
+ headers=headers,
520
+ timeout=30
521
+ )
522
+
523
+ if response.status_code == 200:
524
+ try:
525
+ rooms = response.json()
526
+ return rooms if isinstance(rooms, list) else []
527
+ except:
528
+ return []
529
+ else:
530
+ print(f"❌ Room finder failed: {response.status_code}")
531
+ return []
532
+
533
+ except Exception as e:
534
+ print(f"❌ Error finding rooms: {e}")
535
+ return []
536
+
537
+ def get_available_rooms(self, clan_id: str = None) -> Dict[str, Any]:
538
+ """
539
+ Get comprehensive room information.
540
+
541
+ Args:
542
+ clan_id: Clan ID to filter rooms (optional)
543
+
544
+ Returns:
545
+ Dictionary with room information
546
+ """
547
+ rooms = self.find_rooms(clan_id=clan_id)
548
+
549
+ return {
550
+ "total_rooms": len(rooms),
551
+ "rooms": rooms,
552
+ "clan_id": clan_id,
553
+ "timestamp": "2026-02-03 22:10:19"
554
+ }
555
+
556
+ def _parse_access_token(self, access_token: str) -> Dict[str, Any]:
557
+ """
558
+ Parse the access token into its components.
559
+
560
+ Args:
561
+ access_token: The access token string
562
+
563
+ Returns:
564
+ Dictionary with parsed token components
565
+ """
566
+ try:
567
+ # Split the token by commas
568
+ parts = access_token.split(',')
569
+
570
+ if len(parts) >= 6:
571
+ return {
572
+ "token_id": parts[0],
573
+ "scopes": parts[1],
574
+ "client_id": parts[2],
575
+ "timestamp": parts[3],
576
+ "credential": parts[4],
577
+ "device_id": parts[5],
578
+ "signature": parts[6] if len(parts) > 6 else ""
579
+ }
580
+ else:
581
+ return {"raw_token": access_token, "parts": parts}
582
+ except Exception as e:
583
+ return {"error": str(e), "raw_token": access_token}
584
+
585
+ def launch_game_session(self, device_id: str = None) -> Dict[str, Any]:
586
+ """
587
+ Create a federation session and prepare for game launch.
588
+
589
+ Args:
590
+ device_id: Device ID for the session (optional)
591
+
592
+ Returns:
593
+ Dictionary with launch information
594
+ """
595
+ session_data = self.create_federation_session(device_id)
596
+
597
+ if session_data.get("success"):
598
+ return {
599
+ "success": True,
600
+ "message": "Game session ready for launch",
601
+ "room_id": session_data.get("room_id"),
602
+ "controller": {
603
+ "host": session_data.get("controller_host"),
604
+ "tcp_port": session_data.get("controller_tcp_port"),
605
+ "http_port": session_data.get("controller_http_port"),
606
+ "https_port": session_data.get("controller_https_port")
607
+ },
608
+ "access_token": session_data.get("access_token"),
609
+ "encrypted_access_token": session_data.get("encrypted_access_token"),
610
+ "parsed_token": session_data.get("parsed_access_token"),
611
+ "launch_command": self._build_launch_command(session_data)
612
+ }
613
+ else:
614
+ return session_data
615
+
616
+ def _build_launch_command(self, session_data: Dict[str, Any]) -> str:
617
+ """
618
+ Build a launch command for the MC5 game.
619
+
620
+ Args:
621
+ session_data: Session data from federation
622
+
623
+ Returns:
624
+ Launch command string
625
+ """
626
+ host = session_data.get("controller_host", "localhost")
627
+ room_id = session_data.get("room_id", "")
628
+ access_token = session_data.get("access_token", "")
629
+
630
+ # This is a simplified launch command
631
+ return f"mc5://launch?host={host}&room={room_id}&token={access_token}"
632
+
633
+ def get_session_info(self, device_id: str = None) -> Dict[str, Any]:
634
+ """
635
+ Get comprehensive session information.
636
+
637
+ Args:
638
+ device_id: Device ID for the session (optional)
639
+
640
+ Returns:
641
+ Dictionary with session information
642
+ """
643
+ session_data = self.create_federation_session(device_id)
644
+
645
+ if session_data.get("success"):
646
+ return {
647
+ "session": session_data,
648
+ "device_info": self.get_device_info(),
649
+ "global_id": self.get_global_id(device_id),
650
+ "timestamp": session_data.get("parsed_access_token", {}).get("timestamp", "Unknown")
651
+ }
652
+ else:
653
+ return session_data
654
+
655
+
656
+ # === CONVENIENCE FUNCTIONS ===
657
+
658
+ def quick_connect(username: str = None, password: str = None) -> MC5Easy:
659
+ """Quick connect to MC5 with credentials."""
660
+ mc5 = MC5Easy(username, password)
661
+ mc5.connect()
662
+ return mc5
663
+
664
+ def check_my_daily_tasks(username: str = None, password: str = None) -> List[Dict[str, Any]]:
665
+ """Quick check of daily tasks."""
666
+ with quick_connect(username, password) as mc5:
667
+ return mc5.check_daily_tasks()
668
+
669
+ def get_my_mc5_profile(username: str = None, password: str = None) -> Dict[str, Any]:
670
+ """Quick get of profile."""
671
+ with quick_connect(username, password) as mc5:
672
+ return mc5.get_my_stats()
673
+
674
+ def find_mc5_player(dogtag: str, username: str = None, password: str = None) -> Dict[str, Any]:
675
+ """Quick find player."""
676
+ with quick_connect(username, password) as mc5:
677
+ return mc5.find_player(dogtag)
678
+
679
+ def send_mc5_message(dogtag: str, message: str, username: str = None, password: str = None) -> bool:
680
+ """Quick send message."""
681
+ with quick_connect(username, password) as mc5:
682
+ return mc5.send_message_to_friend(dogtag, message)