Add Google Contacts integration and fix critical Windows issues
- Add People API integration with contact management tools (create, list, get) - Fix OAuth flow: replace deprecated OOB with localhost callback - Fix Ctrl+C handling with proper signal handlers for graceful shutdown - Fix UTF-8 encoding in scheduled_tasks.py for Windows compatibility - Add users/ directory to gitignore to protect personal data Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
327
google_tools/people_client.py
Normal file
327
google_tools/people_client.py
Normal file
@@ -0,0 +1,327 @@
|
||||
"""Google People API client for managing contacts."""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from googleapiclient.discovery import build
|
||||
from googleapiclient.errors import HttpError
|
||||
|
||||
from .oauth_manager import GoogleOAuthManager
|
||||
|
||||
PERSON_FIELDS = "names,emailAddresses,phoneNumbers,biographies"
|
||||
|
||||
|
||||
class PeopleClient:
|
||||
"""Client for Google People API operations."""
|
||||
|
||||
def __init__(self, oauth_manager: GoogleOAuthManager):
|
||||
"""Initialize People client.
|
||||
|
||||
Args:
|
||||
oauth_manager: Initialized OAuth manager with valid credentials
|
||||
"""
|
||||
self.oauth_manager = oauth_manager
|
||||
self.service = None
|
||||
self._initialize_service()
|
||||
|
||||
def _initialize_service(self) -> bool:
|
||||
"""Initialize People API service.
|
||||
|
||||
Returns:
|
||||
True if initialized successfully, False otherwise
|
||||
"""
|
||||
credentials = self.oauth_manager.get_credentials()
|
||||
if not credentials:
|
||||
return False
|
||||
|
||||
try:
|
||||
self.service = build("people", "v1", credentials=credentials)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[People] Failed to initialize service: {e}")
|
||||
return False
|
||||
|
||||
def _ensure_service(self) -> bool:
|
||||
"""Ensure the service is initialized."""
|
||||
if not self.service:
|
||||
return self._initialize_service()
|
||||
return True
|
||||
|
||||
def create_contact(
|
||||
self,
|
||||
given_name: str,
|
||||
family_name: str = "",
|
||||
email: str = "",
|
||||
phone: Optional[str] = None,
|
||||
notes: Optional[str] = None,
|
||||
) -> Dict:
|
||||
"""Create a new contact.
|
||||
|
||||
Args:
|
||||
given_name: First name
|
||||
family_name: Last name
|
||||
email: Email address
|
||||
phone: Optional phone number
|
||||
notes: Optional notes/biography
|
||||
|
||||
Returns:
|
||||
Dict with success status and contact details or error
|
||||
"""
|
||||
if not self._ensure_service():
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Not authorized. Run: python bot_runner.py --setup-google",
|
||||
}
|
||||
|
||||
try:
|
||||
body: Dict[str, Any] = {
|
||||
"names": [{"givenName": given_name, "familyName": family_name}],
|
||||
}
|
||||
|
||||
if email:
|
||||
body["emailAddresses"] = [{"value": email}]
|
||||
|
||||
if phone:
|
||||
body["phoneNumbers"] = [{"value": phone}]
|
||||
|
||||
if notes:
|
||||
body["biographies"] = [{"value": notes, "contentType": "TEXT_PLAIN"}]
|
||||
|
||||
result = (
|
||||
self.service.people()
|
||||
.createContact(body=body)
|
||||
.execute()
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"resource_name": result.get("resourceName", ""),
|
||||
"name": f"{given_name} {family_name}".strip(),
|
||||
}
|
||||
|
||||
except HttpError as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def list_contacts(
|
||||
self,
|
||||
max_results: int = 100,
|
||||
query: Optional[str] = None,
|
||||
) -> Dict:
|
||||
"""List or search contacts.
|
||||
|
||||
Args:
|
||||
max_results: Maximum number of contacts to return (max: 1000)
|
||||
query: Optional search query string
|
||||
|
||||
Returns:
|
||||
Dict with success status and contacts or error
|
||||
"""
|
||||
if not self._ensure_service():
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Not authorized. Run: python bot_runner.py --setup-google",
|
||||
}
|
||||
|
||||
try:
|
||||
max_results = min(max_results, 1000)
|
||||
|
||||
if query:
|
||||
# Use searchContacts for query-based search
|
||||
result = (
|
||||
self.service.people()
|
||||
.searchContacts(
|
||||
query=query,
|
||||
readMask="names,emailAddresses,phoneNumbers,biographies",
|
||||
pageSize=min(max_results, 30), # searchContacts max is 30
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
people = [r["person"] for r in result.get("results", [])]
|
||||
else:
|
||||
# Use connections.list for full listing
|
||||
result = (
|
||||
self.service.people()
|
||||
.connections()
|
||||
.list(
|
||||
resourceName="people/me",
|
||||
pageSize=max_results,
|
||||
personFields=PERSON_FIELDS,
|
||||
sortOrder="LAST_NAME_ASCENDING",
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
people = result.get("connections", [])
|
||||
|
||||
contacts = [self._format_contact(p) for p in people]
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"contacts": contacts,
|
||||
"count": len(contacts),
|
||||
"summary": self._format_contacts_summary(contacts),
|
||||
}
|
||||
|
||||
except HttpError as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def get_contact(self, resource_name: str) -> Dict:
|
||||
"""Get a single contact by resource name.
|
||||
|
||||
Args:
|
||||
resource_name: Contact resource name (e.g., "people/c1234567890")
|
||||
|
||||
Returns:
|
||||
Dict with success status and contact details or error
|
||||
"""
|
||||
if not self._ensure_service():
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Not authorized. Run: python bot_runner.py --setup-google",
|
||||
}
|
||||
|
||||
try:
|
||||
result = (
|
||||
self.service.people()
|
||||
.get(resourceName=resource_name, personFields=PERSON_FIELDS)
|
||||
.execute()
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"contact": self._format_contact(result),
|
||||
}
|
||||
|
||||
except HttpError as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def update_contact(self, resource_name: str, updates: Dict) -> Dict:
|
||||
"""Update an existing contact.
|
||||
|
||||
Args:
|
||||
resource_name: Contact resource name (e.g., "people/c1234567890")
|
||||
updates: Dict with fields to update (given_name, family_name, email, phone, notes)
|
||||
|
||||
Returns:
|
||||
Dict with success status or error
|
||||
"""
|
||||
if not self._ensure_service():
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Not authorized. Run: python bot_runner.py --setup-google",
|
||||
}
|
||||
|
||||
try:
|
||||
# Get current contact to obtain etag
|
||||
current = (
|
||||
self.service.people()
|
||||
.get(resourceName=resource_name, personFields=PERSON_FIELDS)
|
||||
.execute()
|
||||
)
|
||||
|
||||
body: Dict[str, Any] = {"etag": current["etag"]}
|
||||
update_fields = []
|
||||
|
||||
if "given_name" in updates or "family_name" in updates:
|
||||
names = current.get("names", [{}])
|
||||
name = names[0] if names else {}
|
||||
body["names"] = [{
|
||||
"givenName": updates.get("given_name", name.get("givenName", "")),
|
||||
"familyName": updates.get("family_name", name.get("familyName", "")),
|
||||
}]
|
||||
update_fields.append("names")
|
||||
|
||||
if "email" in updates:
|
||||
body["emailAddresses"] = [{"value": updates["email"]}]
|
||||
update_fields.append("emailAddresses")
|
||||
|
||||
if "phone" in updates:
|
||||
body["phoneNumbers"] = [{"value": updates["phone"]}]
|
||||
update_fields.append("phoneNumbers")
|
||||
|
||||
if "notes" in updates:
|
||||
body["biographies"] = [{"value": updates["notes"], "contentType": "TEXT_PLAIN"}]
|
||||
update_fields.append("biographies")
|
||||
|
||||
if not update_fields:
|
||||
return {"success": False, "error": "No valid fields to update"}
|
||||
|
||||
result = (
|
||||
self.service.people()
|
||||
.updateContact(
|
||||
resourceName=resource_name,
|
||||
body=body,
|
||||
updatePersonFields=",".join(update_fields),
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"resource_name": result.get("resourceName", resource_name),
|
||||
"updated_fields": update_fields,
|
||||
}
|
||||
|
||||
except HttpError as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def delete_contact(self, resource_name: str) -> Dict:
|
||||
"""Delete a contact.
|
||||
|
||||
Args:
|
||||
resource_name: Contact resource name (e.g., "people/c1234567890")
|
||||
|
||||
Returns:
|
||||
Dict with success status or error
|
||||
"""
|
||||
if not self._ensure_service():
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Not authorized. Run: python bot_runner.py --setup-google",
|
||||
}
|
||||
|
||||
try:
|
||||
self.service.people().deleteContact(
|
||||
resourceName=resource_name,
|
||||
).execute()
|
||||
|
||||
return {"success": True, "deleted": resource_name}
|
||||
|
||||
except HttpError as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def _format_contact(self, person: Dict) -> Dict:
|
||||
"""Format a person resource into a simple contact dict."""
|
||||
names = person.get("names", [])
|
||||
emails = person.get("emailAddresses", [])
|
||||
phones = person.get("phoneNumbers", [])
|
||||
bios = person.get("biographies", [])
|
||||
|
||||
name = names[0] if names else {}
|
||||
|
||||
return {
|
||||
"resource_name": person.get("resourceName", ""),
|
||||
"given_name": name.get("givenName", ""),
|
||||
"family_name": name.get("familyName", ""),
|
||||
"display_name": name.get("displayName", ""),
|
||||
"email": emails[0].get("value", "") if emails else "",
|
||||
"phone": phones[0].get("value", "") if phones else "",
|
||||
"notes": bios[0].get("value", "") if bios else "",
|
||||
}
|
||||
|
||||
def _format_contacts_summary(self, contacts: List[Dict]) -> str:
|
||||
"""Format contacts into a readable summary."""
|
||||
if not contacts:
|
||||
return "No contacts found."
|
||||
|
||||
lines = []
|
||||
for i, c in enumerate(contacts, 1):
|
||||
name = c.get("display_name") or f"{c.get('given_name', '')} {c.get('family_name', '')}".strip()
|
||||
parts = [f"{i}. {name or '(no name)'}"]
|
||||
|
||||
if c.get("email"):
|
||||
parts.append(f" Email: {c['email']}")
|
||||
if c.get("phone"):
|
||||
parts.append(f" Phone: {c['phone']}")
|
||||
|
||||
lines.append("\n".join(parts))
|
||||
|
||||
return "\n".join(lines)
|
||||
Reference in New Issue
Block a user