feat: Remove deprecated test files and enhance documentation for version 3.3.0 "Nexus Infinitum"
- Deleted obsolete test files: test_mime.go, test_mime_integration.go, and xmpp_client_upload_diagnosis.ipynb. - Updated README.md to reflect the new version 3.3.0 "Nexus Infinitum" and its features. - Added comprehensive release notes for version 3.3.0 detailing major enhancements and installation instructions. - Introduced cleanup script to remove development artifacts while preserving production files.
This commit is contained in:
@ -1,481 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "050a107f",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 🔍 XMPP Client Upload Authentication Diagnosis\n",
|
||||
"\n",
|
||||
"**Problem Analysis:** Dino and Gajim can't upload after restart, Android works after reconnection\n",
|
||||
"\n",
|
||||
"**Network Setup:**\n",
|
||||
"- Desktop: WLAN + Ethernet → Router → HMAC File Server\n",
|
||||
"- Mobile: Android XMPP client → Router → HMAC File Server\n",
|
||||
"\n",
|
||||
"**Date:** August 26, 2025"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "b6a2684e",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 🎯 Problem Identification\n",
|
||||
"\n",
|
||||
"### Symptoms:\n",
|
||||
"- ❌ **Dino (Desktop):** Upload fails after restart\n",
|
||||
"- ❌ **Gajim (Desktop):** Upload fails after restart \n",
|
||||
"- ✅ **Android:** Upload works after disconnect/reconnect\n",
|
||||
"\n",
|
||||
"### Network Context:\n",
|
||||
"- Notebook with WLAN + Ethernet (dual interface)\n",
|
||||
"- Router provides access to HMAC File Server\n",
|
||||
"- Fixed connections vs mobile reconnection behavior"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "b04688cd",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Check current server status and configuration\n",
|
||||
"import subprocess\n",
|
||||
"import json\n",
|
||||
"from datetime import datetime\n",
|
||||
"\n",
|
||||
"print(\"🔍 HMAC File Server Status Check\")\n",
|
||||
"print(\"=\" * 40)\n",
|
||||
"\n",
|
||||
"# Check if server is running\n",
|
||||
"try:\n",
|
||||
" result = subprocess.run(['ps', 'aux'], capture_output=True, text=True)\n",
|
||||
" if 'hmac-file-server' in result.stdout:\n",
|
||||
" print(\"✅ HMAC File Server is running\")\n",
|
||||
" \n",
|
||||
" # Extract server process info\n",
|
||||
" for line in result.stdout.split('\\n'):\n",
|
||||
" if 'hmac-file-server' in line and 'grep' not in line:\n",
|
||||
" print(f\"📊 Process: {line.split()[1]} {' '.join(line.split()[10:])}\")\n",
|
||||
" else:\n",
|
||||
" print(\"❌ HMAC File Server not running\")\n",
|
||||
"except Exception as e:\n",
|
||||
" print(f\"⚠️ Could not check server status: {e}\")\n",
|
||||
"\n",
|
||||
"print(f\"\\n🕐 Check time: {datetime.now()}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "86dc3450",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 🔍 Root Cause Analysis\n",
|
||||
"\n",
|
||||
"### Likely Issues:\n",
|
||||
"\n",
|
||||
"#### 1. **Token Expiration vs Session Management**\n",
|
||||
"- Desktop clients (Dino/Gajim) may cache expired tokens after restart\n",
|
||||
"- Android reconnection triggers fresh token generation\n",
|
||||
"- Grace periods may not apply to cached tokens\n",
|
||||
"\n",
|
||||
"#### 2. **Network Interface Detection**\n",
|
||||
"- Dual interface (WLAN + Ethernet) may confuse IP detection\n",
|
||||
"- Desktop clients may use different IP after restart\n",
|
||||
"- Router NAT may assign different internal IPs\n",
|
||||
"\n",
|
||||
"#### 3. **Client Behavior Differences**\n",
|
||||
"- Desktop clients: Restore session from disk cache\n",
|
||||
"- Mobile clients: Fresh authentication after reconnect\n",
|
||||
"- Token validation may be stricter for cached sessions"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "1bcfae8c",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Check network configuration and IP detection\n",
|
||||
"print(\"🌐 Network Configuration Analysis\")\n",
|
||||
"print(\"=\" * 40)\n",
|
||||
"\n",
|
||||
"# Check network interfaces\n",
|
||||
"try:\n",
|
||||
" result = subprocess.run(['ip', 'addr', 'show'], capture_output=True, text=True)\n",
|
||||
" interfaces = []\n",
|
||||
" current_interface = None\n",
|
||||
" \n",
|
||||
" for line in result.stdout.split('\\n'):\n",
|
||||
" if ': ' in line and ('wlan' in line or 'eth' in line or 'eno' in line or 'wlp' in line):\n",
|
||||
" current_interface = line.split(':')[1].strip().split('@')[0]\n",
|
||||
" interfaces.append(current_interface)\n",
|
||||
" elif current_interface and 'inet ' in line and '127.0.0.1' not in line:\n",
|
||||
" ip = line.strip().split()[1].split('/')[0]\n",
|
||||
" print(f\"📡 Interface {current_interface}: {ip}\")\n",
|
||||
" \n",
|
||||
" print(f\"\\n🔌 Total network interfaces found: {len(interfaces)}\")\n",
|
||||
" if len(interfaces) > 1:\n",
|
||||
" print(\"⚠️ Multiple interfaces detected - potential IP confusion for clients\")\n",
|
||||
" \n",
|
||||
"except Exception as e:\n",
|
||||
" print(f\"⚠️ Could not analyze network interfaces: {e}\")\n",
|
||||
"\n",
|
||||
"# Check routing table\n",
|
||||
"try:\n",
|
||||
" result = subprocess.run(['ip', 'route', 'show'], capture_output=True, text=True)\n",
|
||||
" print(\"\\n🛣️ Default routes:\")\n",
|
||||
" for line in result.stdout.split('\\n'):\n",
|
||||
" if 'default' in line:\n",
|
||||
" print(f\" {line}\")\n",
|
||||
"except Exception as e:\n",
|
||||
" print(f\"⚠️ Could not check routing: {e}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "44dabca1",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 📊 Bearer Token Analysis\n",
|
||||
"\n",
|
||||
"Let's examine how the HMAC File Server handles different client scenarios:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "bbfe7fe4",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Analyze Bearer token validation logic\n",
|
||||
"print(\"🔐 Bearer Token Validation Analysis\")\n",
|
||||
"print(\"=\" * 40)\n",
|
||||
"\n",
|
||||
"# Check if the enhanced validation function exists\n",
|
||||
"try:\n",
|
||||
" with open('/root/hmac-file-server/cmd/server/main.go', 'r') as f:\n",
|
||||
" content = f.read()\n",
|
||||
" \n",
|
||||
" # Look for mobile client detection\n",
|
||||
" if 'isMobileXMPP' in content:\n",
|
||||
" print(\"✅ Mobile XMPP client detection enabled\")\n",
|
||||
" \n",
|
||||
" # Extract mobile detection logic\n",
|
||||
" lines = content.split('\\n')\n",
|
||||
" in_mobile_section = False\n",
|
||||
" for i, line in enumerate(lines):\n",
|
||||
" if 'isMobileXMPP.*:=' in line or 'isMobileXMPP =' in line:\n",
|
||||
" in_mobile_section = True\n",
|
||||
" print(\"\\n📱 Mobile client detection logic:\")\n",
|
||||
" elif in_mobile_section and 'conversations' in line.lower():\n",
|
||||
" print(f\" - Conversations: {'✅' if 'conversations' in line else '❌'}\")\n",
|
||||
" elif in_mobile_section and 'dino' in line.lower():\n",
|
||||
" print(f\" - Dino: {'✅' if 'dino' in line else '❌'}\")\n",
|
||||
" elif in_mobile_section and 'gajim' in line.lower():\n",
|
||||
" print(f\" - Gajim: {'✅' if 'gajim' in line else '❌'}\")\n",
|
||||
" elif in_mobile_section and 'android' in line.lower():\n",
|
||||
" print(f\" - Android: {'✅' if 'android' in line else '❌'}\")\n",
|
||||
" elif in_mobile_section and ('}' in line or 'if ' in line):\n",
|
||||
" in_mobile_section = False\n",
|
||||
" \n",
|
||||
" # Check grace period configuration\n",
|
||||
" if 'gracePeriod' in content:\n",
|
||||
" print(\"\\n⏰ Grace period configuration:\")\n",
|
||||
" for line in content.split('\\n'):\n",
|
||||
" if 'gracePeriod.*=' in line and ('28800' in line or '43200' in line or '86400' in line or '259200' in line):\n",
|
||||
" if '28800' in line:\n",
|
||||
" print(\" - Base grace: 8 hours (28800s)\")\n",
|
||||
" elif '43200' in line:\n",
|
||||
" print(\" - Mobile grace: 12 hours (43200s)\")\n",
|
||||
" elif '86400' in line:\n",
|
||||
" print(\" - Network resilience: 24 hours (86400s)\")\n",
|
||||
" elif '259200' in line:\n",
|
||||
" print(\" - Ultra grace: 72 hours (259200s)\")\n",
|
||||
" \n",
|
||||
"except Exception as e:\n",
|
||||
" print(f\"⚠️ Could not analyze Bearer token validation: {e}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "5527fdcc",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 🎯 Specific Problem: Desktop vs Mobile Client Behavior\n",
|
||||
"\n",
|
||||
"### The Issue:\n",
|
||||
"1. **Desktop clients (Dino/Gajim)** restore sessions from cache after restart\n",
|
||||
"2. **Cached tokens may be expired** or tied to old IP addresses\n",
|
||||
"3. **Mobile clients get fresh tokens** when reconnecting\n",
|
||||
"4. **Grace periods may not apply** to restored cached sessions"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "dcfb3356",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Check server logs for authentication failures\n",
|
||||
"print(\"📋 Recent Authentication Activity\")\n",
|
||||
"print(\"=\" * 40)\n",
|
||||
"\n",
|
||||
"log_files = [\n",
|
||||
" '/var/log/hmac-file-server-mobile.log',\n",
|
||||
" '/var/log/hmac-file-server.log',\n",
|
||||
" '/tmp/server.log'\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"for log_file in log_files:\n",
|
||||
" try:\n",
|
||||
" result = subprocess.run(['tail', '-20', log_file], capture_output=True, text=True)\n",
|
||||
" if result.returncode == 0 and result.stdout.strip():\n",
|
||||
" print(f\"\\n📝 Last 20 lines from {log_file}:\")\n",
|
||||
" lines = result.stdout.strip().split('\\n')\n",
|
||||
" for line in lines[-10:]: # Show last 10 lines\n",
|
||||
" if any(keyword in line.lower() for keyword in ['error', 'fail', 'invalid', 'expired', 'bearer', 'auth']):\n",
|
||||
" print(f\"🔍 {line}\")\n",
|
||||
" break\n",
|
||||
" except:\n",
|
||||
" continue\n",
|
||||
" \n",
|
||||
"print(\"\\n💡 Look for patterns like:\")\n",
|
||||
"print(\" - 'Invalid Bearer token' (expired cached tokens)\")\n",
|
||||
"print(\" - 'expired beyond grace period' (old sessions)\")\n",
|
||||
"print(\" - User-Agent differences between clients\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "41f66318",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 🔧 Solution Strategy\n",
|
||||
"\n",
|
||||
"### Immediate Fixes:\n",
|
||||
"\n",
|
||||
"#### 1. **Clear Client Caches**\n",
|
||||
"- Dino: `~/.local/share/dino/` \n",
|
||||
"- Gajim: `~/.local/share/gajim/`\n",
|
||||
"\n",
|
||||
"#### 2. **Extend Grace Periods for Desktop Clients**\n",
|
||||
"- Treat Dino/Gajim as mobile clients for grace period calculation\n",
|
||||
"- Add specific detection for desktop XMPP clients\n",
|
||||
"\n",
|
||||
"#### 3. **Enhanced Session Recovery**\n",
|
||||
"- Implement session recovery for cached tokens\n",
|
||||
"- Allow IP changes for restored sessions"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "c3054967",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Generate client cache clearing commands\n",
|
||||
"print(\"🧹 Client Cache Clearing Commands\")\n",
|
||||
"print(\"=\" * 40)\n",
|
||||
"\n",
|
||||
"import os\n",
|
||||
"home_dir = os.path.expanduser('~')\n",
|
||||
"\n",
|
||||
"cache_locations = {\n",
|
||||
" 'Dino': [\n",
|
||||
" f'{home_dir}/.local/share/dino/',\n",
|
||||
" f'{home_dir}/.cache/dino/',\n",
|
||||
" f'{home_dir}/.config/dino/'\n",
|
||||
" ],\n",
|
||||
" 'Gajim': [\n",
|
||||
" f'{home_dir}/.local/share/gajim/',\n",
|
||||
" f'{home_dir}/.cache/gajim/',\n",
|
||||
" f'{home_dir}/.config/gajim/'\n",
|
||||
" ]\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"print(\"🔍 Check these locations for cached data:\")\n",
|
||||
"for client, locations in cache_locations.items():\n",
|
||||
" print(f\"\\n📱 {client}:\")\n",
|
||||
" for location in locations:\n",
|
||||
" if os.path.exists(location):\n",
|
||||
" print(f\" ✅ {location} (exists)\")\n",
|
||||
" # List important files\n",
|
||||
" try:\n",
|
||||
" for root, dirs, files in os.walk(location):\n",
|
||||
" for file in files:\n",
|
||||
" if any(keyword in file.lower() for keyword in ['token', 'session', 'cache', 'upload']):\n",
|
||||
" print(f\" 🔍 {os.path.join(root, file)}\")\n",
|
||||
" except:\n",
|
||||
" pass\n",
|
||||
" else:\n",
|
||||
" print(f\" ❌ {location} (not found)\")\n",
|
||||
"\n",
|
||||
"print(\"\\n🚨 MANUAL STEPS TO TRY:\")\n",
|
||||
"print(\"1. Close Dino and Gajim completely\")\n",
|
||||
"print(\"2. Clear application caches (backup first!)\")\n",
|
||||
"print(\"3. Restart clients and test upload\")\n",
|
||||
"print(\"4. If still failing, check server logs for specific errors\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "6dcc992f",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 🛠️ Enhanced Server Configuration\n",
|
||||
"\n",
|
||||
"Let's create an enhanced configuration that treats desktop XMPP clients with the same grace as mobile clients:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "6efe0490",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Check current mobile client detection and suggest improvements\n",
|
||||
"print(\"🔧 Desktop Client Enhancement Strategy\")\n",
|
||||
"print(\"=\" * 40)\n",
|
||||
"\n",
|
||||
"# Read current configuration\n",
|
||||
"try:\n",
|
||||
" with open('/root/hmac-file-server/config-mobile-resilient.toml', 'r') as f:\n",
|
||||
" config = f.read()\n",
|
||||
" \n",
|
||||
" print(\"📄 Current grace period settings:\")\n",
|
||||
" for line in config.split('\\n'):\n",
|
||||
" if 'grace' in line.lower() and '=' in line:\n",
|
||||
" print(f\" {line.strip()}\")\n",
|
||||
" \n",
|
||||
" print(\"\\n💡 Recommended enhancement:\")\n",
|
||||
" print(\" - Treat Dino and Gajim as 'mobile' clients for grace periods\")\n",
|
||||
" print(\" - Add 'desktop_xmpp_grace_period = 24h' for cached session recovery\")\n",
|
||||
" print(\" - Enable session_restoration = true for desktop clients\")\n",
|
||||
" \n",
|
||||
"except Exception as e:\n",
|
||||
" print(f\"⚠️ Could not read config: {e}\")\n",
|
||||
"\n",
|
||||
"# Show the enhanced mobile detection logic needed\n",
|
||||
"print(\"\\n🔍 Enhanced Client Detection Logic Needed:\")\n",
|
||||
"print(\"```go\")\n",
|
||||
"print(\"// Enhanced XMPP client detection (both mobile and desktop)\")\n",
|
||||
"print(\"isXMPPClient := strings.Contains(strings.ToLower(userAgent), \\\"conversations\\\") ||\")\n",
|
||||
"print(\" strings.Contains(strings.ToLower(userAgent), \\\"dino\\\") ||\")\n",
|
||||
"print(\" strings.Contains(strings.ToLower(userAgent), \\\"gajim\\\") ||\")\n",
|
||||
"print(\" strings.Contains(strings.ToLower(userAgent), \\\"android\\\") ||\")\n",
|
||||
"print(\" strings.Contains(strings.ToLower(userAgent), \\\"xmpp\\\")\")\n",
|
||||
"print(\"\")\n",
|
||||
"print(\"// Desktop XMPP clients need same grace as mobile for session restoration\")\n",
|
||||
"print(\"if isXMPPClient {\")\n",
|
||||
"print(\" gracePeriod = int64(86400) // 24 hours for all XMPP clients\")\n",
|
||||
"print(\"}\")\n",
|
||||
"print(\"```\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "6cdcf458",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 🎯 Immediate Action Plan\n",
|
||||
"\n",
|
||||
"### Step 1: Quick Client Fix\n",
|
||||
"1. **Close Dino and Gajim completely**\n",
|
||||
"2. **Clear their caches/sessions** (backup first)\n",
|
||||
"3. **Restart clients** - they should get fresh tokens\n",
|
||||
"\n",
|
||||
"### Step 2: Server Enhancement \n",
|
||||
"1. **Modify mobile client detection** to include desktop XMPP clients\n",
|
||||
"2. **Extend grace periods** for all XMPP clients (not just mobile)\n",
|
||||
"3. **Add session restoration** logic for cached tokens\n",
|
||||
"\n",
|
||||
"### Step 3: Network Optimization\n",
|
||||
"1. **Check for IP conflicts** between WLAN/Ethernet\n",
|
||||
"2. **Verify router configuration** for consistent NAT\n",
|
||||
"3. **Monitor upload endpoints** for client-specific issues"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "d1f7580d",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Generate immediate fix commands\n",
|
||||
"print(\"⚡ IMMEDIATE FIX COMMANDS\")\n",
|
||||
"print(\"=\" * 40)\n",
|
||||
"\n",
|
||||
"print(\"1️⃣ STOP XMPP CLIENTS:\")\n",
|
||||
"print(\" pkill -f dino\")\n",
|
||||
"print(\" pkill -f gajim\")\n",
|
||||
"print(\" # Wait 5 seconds\")\n",
|
||||
"\n",
|
||||
"print(\"\\n2️⃣ BACKUP AND CLEAR CACHES:\")\n",
|
||||
"print(\" # Backup first (optional)\")\n",
|
||||
"print(\" cp -r ~/.local/share/dino ~/.local/share/dino.backup\")\n",
|
||||
"print(\" cp -r ~/.local/share/gajim ~/.local/share/gajim.backup\")\n",
|
||||
"print(\" \")\n",
|
||||
"print(\" # Clear session caches\")\n",
|
||||
"print(\" rm -rf ~/.cache/dino/\")\n",
|
||||
"print(\" rm -rf ~/.cache/gajim/\")\n",
|
||||
"print(\" \")\n",
|
||||
"print(\" # Clear specific upload-related files (if they exist)\")\n",
|
||||
"print(\" find ~/.local/share/dino -name '*upload*' -delete 2>/dev/null || true\")\n",
|
||||
"print(\" find ~/.local/share/gajim -name '*upload*' -delete 2>/dev/null || true\")\n",
|
||||
"\n",
|
||||
"print(\"\\n3️⃣ RESTART CLIENTS:\")\n",
|
||||
"print(\" # Start Dino\")\n",
|
||||
"print(\" dino &\")\n",
|
||||
"print(\" \")\n",
|
||||
"print(\" # Start Gajim\")\n",
|
||||
"print(\" gajim &\")\n",
|
||||
"\n",
|
||||
"print(\"\\n4️⃣ TEST UPLOAD:\")\n",
|
||||
"print(\" # Try uploading a small file in both clients\")\n",
|
||||
"print(\" # Check server logs for any authentication issues\")\n",
|
||||
"print(\" tail -f /var/log/hmac-file-server-mobile.log\")\n",
|
||||
"\n",
|
||||
"print(\"\\n🔍 If this doesn't work, the issue is in the server's client detection logic.\")\n",
|
||||
"print(\"The server may not be treating Dino/Gajim with sufficient grace periods.\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "75e3eac8",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 📋 Diagnosis Summary\n",
|
||||
"\n",
|
||||
"### 🎯 **Root Cause**: Session Cache vs Fresh Authentication\n",
|
||||
"\n",
|
||||
"- **Desktop clients (Dino/Gajim)**: Restore cached sessions with potentially expired tokens\n",
|
||||
"- **Mobile clients**: Get fresh authentication after reconnection\n",
|
||||
"- **Server**: May not apply sufficient grace periods to cached/restored sessions\n",
|
||||
"\n",
|
||||
"### ✅ **Solution Priority**:\n",
|
||||
"1. **Immediate**: Clear client caches to force fresh authentication\n",
|
||||
"2. **Short-term**: Enhance server to treat desktop XMPP clients with mobile-level grace\n",
|
||||
"3. **Long-term**: Implement proper session restoration for all XMPP clients\n",
|
||||
"\n",
|
||||
"### 🔧 **Next Steps**:\n",
|
||||
"Execute the immediate fix commands above, then monitor server logs for authentication patterns."
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"language_info": {
|
||||
"name": "python"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
Reference in New Issue
Block a user