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.
- mc5_api_client/__init__.py +31 -0
- mc5_api_client/auth.py +281 -0
- mc5_api_client/cli.py +463 -0
- mc5_api_client/client.py +1220 -0
- mc5_api_client/exceptions.py +78 -0
- mc5_api_client/py.typed +0 -0
- mc5_api_client-1.0.0.dist-info/LICENSE +21 -0
- mc5_api_client-1.0.0.dist-info/METADATA +1150 -0
- mc5_api_client-1.0.0.dist-info/RECORD +12 -0
- mc5_api_client-1.0.0.dist-info/WHEEL +5 -0
- mc5_api_client-1.0.0.dist-info/entry_points.txt +2 -0
- mc5_api_client-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# ────────────[ CHIZOBA ]────────────────────────────
|
|
2
|
+
# | Email : chizoba2026@hotmail.com
|
|
3
|
+
# | File : __init__.py
|
|
4
|
+
# | License : MIT License © 2026 Chizoba
|
|
5
|
+
# | Brief : Modern Combat 5 API Client Package
|
|
6
|
+
# ────────────────★─────────────────────────────────
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
Modern Combat 5 API Client
|
|
10
|
+
|
|
11
|
+
A comprehensive Python library for interacting with the Modern Combat 5 API.
|
|
12
|
+
Provides easy access to authentication, profile management, clan operations,
|
|
13
|
+
messaging, and more.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
__version__ = "1.0.0"
|
|
17
|
+
__author__ = "Chizoba"
|
|
18
|
+
__email__ = "chizoba2026@hotmail.com"
|
|
19
|
+
__license__ = "MIT"
|
|
20
|
+
|
|
21
|
+
from .client import MC5Client
|
|
22
|
+
from .auth import TokenGenerator
|
|
23
|
+
from .exceptions import MC5APIError, AuthenticationError, RateLimitError
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"MC5Client",
|
|
27
|
+
"TokenGenerator",
|
|
28
|
+
"MC5APIError",
|
|
29
|
+
"AuthenticationError",
|
|
30
|
+
"RateLimitError"
|
|
31
|
+
]
|
mc5_api_client/auth.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# ────────────[ CHIZOBA ]────────────────────────────
|
|
2
|
+
# | Email : chizoba2026@hotmail.com
|
|
3
|
+
# | File : auth.py
|
|
4
|
+
# | License : MIT License © 2026 Chizoba
|
|
5
|
+
# | Brief : Authentication and token management for MC5 API
|
|
6
|
+
# ────────────────★─────────────────────────────────
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
Authentication and token management for Modern Combat 5 API.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import time
|
|
13
|
+
import hashlib
|
|
14
|
+
import secrets
|
|
15
|
+
from typing import Optional, Dict, Any
|
|
16
|
+
from datetime import datetime, timedelta
|
|
17
|
+
|
|
18
|
+
import requests
|
|
19
|
+
from requests.adapters import HTTPAdapter
|
|
20
|
+
from urllib3.util.retry import Retry
|
|
21
|
+
|
|
22
|
+
from .exceptions import (
|
|
23
|
+
AuthenticationError,
|
|
24
|
+
InvalidCredentialsError,
|
|
25
|
+
TokenExpiredError,
|
|
26
|
+
NetworkError
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TokenGenerator:
|
|
31
|
+
"""
|
|
32
|
+
Handles authentication and token generation for MC5 API.
|
|
33
|
+
|
|
34
|
+
Supports both user authentication and admin authentication.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
client_id: str = "1875:55979:6.0.0a:windows:windows",
|
|
40
|
+
auth_url: str = "https://eur-janus.gameloft.com:443/authorize",
|
|
41
|
+
timeout: int = 30,
|
|
42
|
+
max_retries: int = 3
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
Initialize the TokenGenerator.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
client_id: Game client identifier
|
|
49
|
+
auth_url: Authentication endpoint URL
|
|
50
|
+
timeout: Request timeout in seconds
|
|
51
|
+
max_retries: Maximum number of retry attempts
|
|
52
|
+
"""
|
|
53
|
+
self.client_id = client_id
|
|
54
|
+
self.auth_url = auth_url
|
|
55
|
+
self.timeout = timeout
|
|
56
|
+
self.max_retries = max_retries
|
|
57
|
+
|
|
58
|
+
# Setup session with retry strategy
|
|
59
|
+
self.session = requests.Session()
|
|
60
|
+
retry_strategy = Retry(
|
|
61
|
+
total=max_retries,
|
|
62
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
|
63
|
+
allowed_methods=["HEAD", "GET", "POST"],
|
|
64
|
+
backoff_factor=1
|
|
65
|
+
)
|
|
66
|
+
adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
67
|
+
self.session.mount("http://", adapter)
|
|
68
|
+
self.session.mount("https://", adapter)
|
|
69
|
+
|
|
70
|
+
# Default headers
|
|
71
|
+
self.session.headers.update({
|
|
72
|
+
"User-Agent": "MC5-API-Client/1.0.0",
|
|
73
|
+
"Accept": "*/*",
|
|
74
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
75
|
+
"Connection": "close"
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
def generate_device_id(self) -> str:
|
|
79
|
+
"""
|
|
80
|
+
Generate a random device ID.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Random 19-digit device ID
|
|
84
|
+
"""
|
|
85
|
+
return str(secrets.randbelow(10**19)).zfill(19)
|
|
86
|
+
|
|
87
|
+
def generate_token(
|
|
88
|
+
self,
|
|
89
|
+
username: str,
|
|
90
|
+
password: str,
|
|
91
|
+
device_id: Optional[str] = None,
|
|
92
|
+
scope: str = "alert auth chat leaderboard_ro lobby message session social config storage_ro tracking_bi feed storage leaderboard_admin social_eve social soc transaction schedule lottery voice matchmaker"
|
|
93
|
+
) -> Dict[str, Any]:
|
|
94
|
+
"""
|
|
95
|
+
Generate an access token for the MC5 API.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
username: MC5 username (e.g., "anonymous:d2luOF92M18xNzU4NjQxNTE3Xy9bF5EFTbvwR7w1nqg2JnI=")
|
|
99
|
+
password: Account password
|
|
100
|
+
device_id: Device ID (generated if not provided)
|
|
101
|
+
scope: Space-separated list of permission scopes
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Dictionary containing token information
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
InvalidCredentialsError: If credentials are invalid
|
|
108
|
+
AuthenticationError: If authentication fails for other reasons
|
|
109
|
+
NetworkError: If network issues occur
|
|
110
|
+
"""
|
|
111
|
+
if device_id is None:
|
|
112
|
+
device_id = self.generate_device_id()
|
|
113
|
+
|
|
114
|
+
payload = {
|
|
115
|
+
"client_id": self.client_id,
|
|
116
|
+
"username": username,
|
|
117
|
+
"password": password,
|
|
118
|
+
"scope": scope,
|
|
119
|
+
"device_id": device_id
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
print(f"🔵 Generating token for user: {username[:20]}...")
|
|
124
|
+
response = self.session.post(
|
|
125
|
+
self.auth_url,
|
|
126
|
+
data=payload,
|
|
127
|
+
timeout=self.timeout
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if response.status_code == 200:
|
|
131
|
+
token_data = response.json()
|
|
132
|
+
access_token = token_data.get("access_token")
|
|
133
|
+
|
|
134
|
+
if not access_token:
|
|
135
|
+
raise AuthenticationError("No access token in response")
|
|
136
|
+
|
|
137
|
+
# Parse token components
|
|
138
|
+
token_parts = access_token.split(",")
|
|
139
|
+
if len(token_parts) < 6:
|
|
140
|
+
raise AuthenticationError("Invalid token format")
|
|
141
|
+
|
|
142
|
+
result = {
|
|
143
|
+
"access_token": access_token,
|
|
144
|
+
"token_id": token_parts[0],
|
|
145
|
+
"scopes": token_parts[1].split(" "),
|
|
146
|
+
"client_id": token_parts[2],
|
|
147
|
+
"expires_at": float(token_parts[3]),
|
|
148
|
+
"credential": token_parts[4],
|
|
149
|
+
"device_id": token_parts[5],
|
|
150
|
+
"signature": token_parts[6] if len(token_parts) > 6 else None,
|
|
151
|
+
"generated_at": time.time(),
|
|
152
|
+
"device_id_used": device_id
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# Calculate expiry time
|
|
156
|
+
expiry_timestamp = result["expires_at"]
|
|
157
|
+
expiry_datetime = datetime.fromtimestamp(expiry_timestamp)
|
|
158
|
+
result["expires_in"] = expiry_timestamp - time.time()
|
|
159
|
+
result["expires_at_datetime"] = expiry_datetime
|
|
160
|
+
|
|
161
|
+
print(f"🟢 Token generated successfully! Expires: {expiry_datetime}")
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
elif response.status_code == 401:
|
|
165
|
+
raise InvalidCredentialsError("Invalid username or password")
|
|
166
|
+
elif response.status_code == 403:
|
|
167
|
+
raise AuthenticationError("Access forbidden - check client permissions")
|
|
168
|
+
else:
|
|
169
|
+
error_msg = f"Authentication failed with status {response.status_code}"
|
|
170
|
+
try:
|
|
171
|
+
error_data = response.json()
|
|
172
|
+
error_msg += f": {error_data.get('error_description', error_data.get('error', 'Unknown error'))}"
|
|
173
|
+
except:
|
|
174
|
+
pass
|
|
175
|
+
raise AuthenticationError(error_msg)
|
|
176
|
+
|
|
177
|
+
except requests.exceptions.Timeout:
|
|
178
|
+
raise NetworkError("Authentication request timed out")
|
|
179
|
+
except requests.exceptions.ConnectionError:
|
|
180
|
+
raise NetworkError("Failed to connect to authentication server")
|
|
181
|
+
except requests.exceptions.RequestException as e:
|
|
182
|
+
raise NetworkError(f"Network error during authentication: {str(e)}")
|
|
183
|
+
|
|
184
|
+
def generate_admin_token(
|
|
185
|
+
self,
|
|
186
|
+
device_id: Optional[str] = None,
|
|
187
|
+
scope: str = "alert auth chat leaderboard_ro lobby message session social config storage_ro tracking_bi"
|
|
188
|
+
) -> Dict[str, Any]:
|
|
189
|
+
"""
|
|
190
|
+
Generate an admin access token.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
device_id: Device ID (generated if not provided)
|
|
194
|
+
scope: Admin permission scopes
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Dictionary containing admin token information
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
AuthenticationError: If admin authentication fails
|
|
201
|
+
"""
|
|
202
|
+
return self.generate_token(
|
|
203
|
+
username="game:mc5_system",
|
|
204
|
+
password="admin",
|
|
205
|
+
device_id=device_id,
|
|
206
|
+
scope=scope
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def validate_token(self, token_data: Dict[str, Any]) -> bool:
|
|
210
|
+
"""
|
|
211
|
+
Check if a token is still valid.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
token_data: Token data from generate_token()
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
True if token is valid, False otherwise
|
|
218
|
+
"""
|
|
219
|
+
if not token_data or "expires_at" not in token_data:
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
# Add 60 second buffer to account for clock skew
|
|
223
|
+
expiry_time = token_data["expires_at"] - 60
|
|
224
|
+
current_time = time.time()
|
|
225
|
+
|
|
226
|
+
return current_time < expiry_time
|
|
227
|
+
|
|
228
|
+
def refresh_token_if_needed(
|
|
229
|
+
self,
|
|
230
|
+
token_data: Dict[str, Any],
|
|
231
|
+
username: str,
|
|
232
|
+
password: str,
|
|
233
|
+
device_id: Optional[str] = None
|
|
234
|
+
) -> Dict[str, Any]:
|
|
235
|
+
"""
|
|
236
|
+
Refresh token if it's expired or will expire soon.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
token_data: Current token data
|
|
240
|
+
username: Account username
|
|
241
|
+
password: Account password
|
|
242
|
+
device_id: Device ID used for original token
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
New token data if refresh was needed, original data otherwise
|
|
246
|
+
"""
|
|
247
|
+
if self.validate_token(token_data):
|
|
248
|
+
print("🟢 Current token is still valid")
|
|
249
|
+
return token_data
|
|
250
|
+
|
|
251
|
+
print("🟠 Token expired, generating new token...")
|
|
252
|
+
return self.generate_token(username, password, device_id or token_data.get("device_id_used"))
|
|
253
|
+
|
|
254
|
+
def get_token_info(self, access_token: str) -> Dict[str, Any]:
|
|
255
|
+
"""
|
|
256
|
+
Parse and return information about an access token.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
access_token: Raw access token string
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Parsed token information
|
|
263
|
+
"""
|
|
264
|
+
parts = access_token.split(",")
|
|
265
|
+
if len(parts) < 6:
|
|
266
|
+
raise AuthenticationError("Invalid token format")
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
"token_id": parts[0],
|
|
270
|
+
"scopes": parts[1].split(" "),
|
|
271
|
+
"client_id": parts[2],
|
|
272
|
+
"expires_at": float(parts[3]),
|
|
273
|
+
"credential": parts[4],
|
|
274
|
+
"device_id": parts[5],
|
|
275
|
+
"signature": parts[6] if len(parts) > 6 else None
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
def close(self):
|
|
279
|
+
"""Close the HTTP session."""
|
|
280
|
+
if self.session:
|
|
281
|
+
self.session.close()
|