Files
ajarbot/google_tools/oauth_manager.py

218 lines
7.7 KiB
Python
Raw Normal View History

"""OAuth2 Manager for Google APIs (Gmail, Calendar, Contacts)."""
import json
import os
import webbrowser
from datetime import datetime, timedelta
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
Refactor: Remove zombie code, fix bugs, and clean documentation This comprehensive refactoring removes dead code, fixes bugs, and deletes outdated documentation to make the codebase production-ready. ## Files Deleted (16 files) ### Temporary/zombie files (9 files): - nul (Windows artifact) - quick_start.bat (superseded by run.bat) - scripts/proxmox_ssh.py (hardcoded credentials - security risk) - scripts/proxmox_ssh.sh (hardcoded credentials - security risk) - scripts/collection_output.txt (one-time audit output) - scripts/collect-homelab-config.sh (one-off infrastructure script) - scripts/collect-remote.sh (one-off infrastructure script) - memory_workspace/MEMORY.md.old (backup file) - promtail-config-optimized.yaml (misplaced homelab config) ### Outdated documentation (7 files): - MCP_MIGRATION.md (migration complete - 2026-02-15) - QUICK_REFERENCE_AGENT_SDK.md (orphaned from cleanup) - SETUP.md (duplicate of README.md quick start) - WINDOWS_QUICK_REFERENCE.md (duplicate of docs/WINDOWS_DEPLOYMENT.md) - SUB_AGENTS.md (design doc for unimplemented feature) - JARVIS_VOICE_INTEGRATION_PLAN.md (1300-line spec, code not implemented) - OBSIDIAN_MCP_SETUP_INSTRUCTIONS.md (temporary troubleshooting doc) - LOGGING.md (redundant with well-commented logging_config.py) - docs/SECURITY_AUDIT_SUMMARY.md (completed audit from 2026-02-12) ## Critical Bug Fixes (2 bugs) 1. bot_runner.py line 122: Fixed wrong dict key reference - Changed send_to_platform → send_to - Bug caused scheduled task platform info to never print 2. usage_tracker.py: Added missing pricing for claude-sonnet-4-6 - Model was default but had no pricing entry - Caused cost under-reporting in Direct API mode ## Code Removed (14 files modified, ~1200 lines deleted) ### Dead imports removed (9 imports): - bot_runner.py: sys - agent.py: time - adapters/runtime.py: re - adapters/skill_integration.py: subprocess - tools.py: redundant Path import - mcp_servers/loki/loki_server.py: json - google_tools/oauth_manager.py: Thread, Dict - google_tools/gmail_client.py: os - google_tools/utils.py: email ### Unused functions/methods removed (9 functions): - agent.py: MEMORY_RESPONSE_PREVIEW_LENGTH constant - scheduled_tasks.py: integrate_scheduler_with_runtime() - adapters/runtime.py: command_preprocessor(), markdown_postprocessor() - adapters/skill_integration.py: invoke_skill_via_cli(), __main__ block - tools.py: _extract_mcp_result() - google_tools/oauth_manager.py: needs_refresh_soon(), revoke_authorization() - google_tools/people_client.py: update_contact(), delete_contact() ### Code quality improvements: - memory_system.py: Removed empty else: pass branch - calendar_client.py: Fixed bare except: → except Exception: - mcp_ssh.py: Updated asyncio.get_event_loop() → get_running_loop() - calendar_client.py: Fixed deprecated datetime.utcnow() → now(timezone.utc) ## Impact - ~1200 lines of dead code removed - 16 obsolete files deleted - 2 critical bugs fixed - 3 deprecated APIs updated - Zero functionality broken (all changes verified) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-24 12:46:56 -07:00
from typing import Optional
from urllib.parse import parse_qs, urlparse
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
# OAuth scopes
SCOPES = [
"https://www.googleapis.com/auth/gmail.modify",
"https://www.googleapis.com/auth/calendar",
"https://www.googleapis.com/auth/contacts",
]
# Token file location
TOKEN_FILE = Path("config/google_oauth_token.json")
CREDENTIALS_FILE = Path("config/google_credentials.yaml")
class OAuthCallbackHandler(BaseHTTPRequestHandler):
"""Handles OAuth2 callback from browser."""
authorization_code: Optional[str] = None
def do_GET(self) -> None:
"""Handle GET request from OAuth callback."""
query = urlparse(self.path).query
params = parse_qs(query)
if "code" in params:
OAuthCallbackHandler.authorization_code = params["code"][0]
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(
b"<html><body><h1>Authorization successful!</h1>"
b"<p>You can close this window and return to the terminal.</p>"
b"</body></html>"
)
else:
self.send_response(400)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(
b"<html><body><h1>Authorization failed</h1>"
b"<p>No authorization code received.</p></body></html>"
)
def log_message(self, format: str, *args) -> None:
"""Suppress logging to keep console clean."""
pass
class GoogleOAuthManager:
"""Manages OAuth2 authentication for Google APIs."""
def __init__(
self,
token_file: Path = TOKEN_FILE,
credentials_file: Path = CREDENTIALS_FILE,
) -> None:
self.token_file = token_file
self.credentials_file = credentials_file
self.credentials: Optional[Credentials] = None
def is_authorized(self) -> bool:
"""Check if valid OAuth tokens exist."""
if not self.token_file.exists():
return False
try:
self.credentials = Credentials.from_authorized_user_file(
str(self.token_file), SCOPES
)
return self.credentials and self.credentials.valid
except Exception:
return False
def get_credentials(self) -> Optional[Credentials]:
"""Get valid OAuth credentials, refreshing if needed.
Returns:
Credentials object if authorized, None otherwise.
"""
# Load existing tokens
if self.token_file.exists():
try:
self.credentials = Credentials.from_authorized_user_file(
str(self.token_file), SCOPES
)
except Exception as e:
print(f"[OAuth] Error loading tokens: {e}")
return None
# Refresh if expired
if self.credentials and not self.credentials.valid:
if self.credentials.expired and self.credentials.refresh_token:
try:
print("[OAuth] Refreshing access token...")
self.credentials.refresh(Request())
self._save_credentials()
print("[OAuth] Token refreshed successfully")
except Exception as e:
print(f"[OAuth] Token refresh failed: {e}")
print("[OAuth] Please run: python bot_runner.py --setup-google")
return None
else:
print("[OAuth] Credentials invalid, re-authorization needed")
print("[OAuth] Please run: python bot_runner.py --setup-google")
return None
return self.credentials
def run_oauth_flow(self, manual: bool = False) -> bool:
"""Run OAuth2 authorization flow.
Args:
manual: If True, requires manual code paste instead of browser callback.
Returns:
True if authorization successful, False otherwise.
"""
if not self.credentials_file.exists():
print(f"[OAuth] Error: {self.credentials_file} not found")
print("[OAuth] Please create config/google_credentials.yaml with:")
print(" client_id: YOUR_CLIENT_ID")
print(" client_secret: YOUR_CLIENT_SECRET")
return False
try:
# Load client credentials
import yaml
with open(self.credentials_file) as f:
creds_config = yaml.safe_load(f)
client_config = {
"installed": {
"client_id": creds_config["client_id"],
"client_secret": creds_config["client_secret"],
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"redirect_uris": ["http://localhost:8081"],
}
}
# Both manual and automatic modes use local server
# (Google deprecated OOB flow in 2022)
flow = InstalledAppFlow.from_client_config(client_config, SCOPES)
flow.redirect_uri = "http://localhost:8081"
auth_url, _ = flow.authorization_url(prompt="consent")
print("\nPlease visit this URL to authorize:")
print(f"\n{auth_url}\n")
# Auto-open browser unless manual mode
if not manual:
try:
webbrowser.open(auth_url)
print("Opening browser automatically...\n")
except Exception:
print("Could not open browser automatically. Please visit the URL above.\n")
else:
print("Please open the URL above in your browser manually.\n")
# Start local server to receive callback
server = HTTPServer(("localhost", 8081), OAuthCallbackHandler)
print("Waiting for authorization... (press Ctrl+C to cancel)\n")
# Wait for callback
server.handle_request()
if OAuthCallbackHandler.authorization_code:
flow.fetch_token(code=OAuthCallbackHandler.authorization_code)
self.credentials = flow.credentials
else:
print("[OAuth] Authorization failed - no code received")
return False
# Save tokens
self._save_credentials()
print("\n✓ Authorization successful!")
print(f"✓ Tokens saved to {self.token_file}\n")
return True
except Exception as e:
print(f"[OAuth] Authorization failed: {e}")
return False
def _save_credentials(self) -> None:
"""Save credentials to token file with secure permissions."""
if not self.credentials:
return
# Ensure config directory exists
self.token_file.parent.mkdir(parents=True, exist_ok=True)
# Write to temp file first (atomic write)
temp_file = self.token_file.with_suffix(".tmp")
with open(temp_file, "w") as f:
f.write(self.credentials.to_json())
# Set file permissions to 600 (owner read/write only) on Unix systems
if os.name != "nt": # Not Windows
os.chmod(temp_file, 0o600)
# Atomic rename
temp_file.replace(self.token_file)