mindroom 0.0.0__py3-none-any.whl → 0.1.1__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.
- mindroom/__init__.py +3 -0
- mindroom/agent_prompts.py +963 -0
- mindroom/agents.py +248 -0
- mindroom/ai.py +421 -0
- mindroom/api/__init__.py +1 -0
- mindroom/api/credentials.py +137 -0
- mindroom/api/google_integration.py +355 -0
- mindroom/api/google_tools_helper.py +40 -0
- mindroom/api/homeassistant_integration.py +421 -0
- mindroom/api/integrations.py +189 -0
- mindroom/api/main.py +506 -0
- mindroom/api/matrix_operations.py +219 -0
- mindroom/api/tools.py +94 -0
- mindroom/background_tasks.py +87 -0
- mindroom/bot.py +2470 -0
- mindroom/cli.py +86 -0
- mindroom/commands.py +377 -0
- mindroom/config.py +343 -0
- mindroom/config_commands.py +324 -0
- mindroom/config_confirmation.py +411 -0
- mindroom/constants.py +52 -0
- mindroom/credentials.py +146 -0
- mindroom/credentials_sync.py +134 -0
- mindroom/custom_tools/__init__.py +8 -0
- mindroom/custom_tools/config_manager.py +765 -0
- mindroom/custom_tools/gmail.py +92 -0
- mindroom/custom_tools/google_calendar.py +92 -0
- mindroom/custom_tools/google_sheets.py +92 -0
- mindroom/custom_tools/homeassistant.py +341 -0
- mindroom/error_handling.py +35 -0
- mindroom/file_watcher.py +49 -0
- mindroom/interactive.py +313 -0
- mindroom/logging_config.py +207 -0
- mindroom/matrix/__init__.py +1 -0
- mindroom/matrix/client.py +782 -0
- mindroom/matrix/event_info.py +173 -0
- mindroom/matrix/identity.py +149 -0
- mindroom/matrix/large_messages.py +267 -0
- mindroom/matrix/mentions.py +141 -0
- mindroom/matrix/message_builder.py +94 -0
- mindroom/matrix/message_content.py +209 -0
- mindroom/matrix/presence.py +178 -0
- mindroom/matrix/rooms.py +311 -0
- mindroom/matrix/state.py +77 -0
- mindroom/matrix/typing.py +91 -0
- mindroom/matrix/users.py +217 -0
- mindroom/memory/__init__.py +21 -0
- mindroom/memory/config.py +137 -0
- mindroom/memory/functions.py +396 -0
- mindroom/py.typed +0 -0
- mindroom/response_tracker.py +128 -0
- mindroom/room_cleanup.py +139 -0
- mindroom/routing.py +107 -0
- mindroom/scheduling.py +758 -0
- mindroom/stop.py +207 -0
- mindroom/streaming.py +203 -0
- mindroom/teams.py +749 -0
- mindroom/thread_utils.py +318 -0
- mindroom/tools/__init__.py +520 -0
- mindroom/tools/agentql.py +64 -0
- mindroom/tools/airflow.py +57 -0
- mindroom/tools/apify.py +49 -0
- mindroom/tools/arxiv.py +64 -0
- mindroom/tools/aws_lambda.py +41 -0
- mindroom/tools/aws_ses.py +57 -0
- mindroom/tools/baidusearch.py +87 -0
- mindroom/tools/brightdata.py +116 -0
- mindroom/tools/browserbase.py +62 -0
- mindroom/tools/cal_com.py +98 -0
- mindroom/tools/calculator.py +112 -0
- mindroom/tools/cartesia.py +84 -0
- mindroom/tools/composio.py +166 -0
- mindroom/tools/config_manager.py +44 -0
- mindroom/tools/confluence.py +73 -0
- mindroom/tools/crawl4ai.py +101 -0
- mindroom/tools/csv.py +104 -0
- mindroom/tools/custom_api.py +106 -0
- mindroom/tools/dalle.py +85 -0
- mindroom/tools/daytona.py +180 -0
- mindroom/tools/discord.py +81 -0
- mindroom/tools/docker.py +73 -0
- mindroom/tools/duckdb.py +124 -0
- mindroom/tools/duckduckgo.py +99 -0
- mindroom/tools/e2b.py +121 -0
- mindroom/tools/eleven_labs.py +77 -0
- mindroom/tools/email.py +74 -0
- mindroom/tools/exa.py +246 -0
- mindroom/tools/fal.py +50 -0
- mindroom/tools/file.py +80 -0
- mindroom/tools/financial_datasets_api.py +112 -0
- mindroom/tools/firecrawl.py +124 -0
- mindroom/tools/gemini.py +85 -0
- mindroom/tools/giphy.py +49 -0
- mindroom/tools/github.py +376 -0
- mindroom/tools/gmail.py +102 -0
- mindroom/tools/google_calendar.py +55 -0
- mindroom/tools/google_maps.py +112 -0
- mindroom/tools/google_sheets.py +86 -0
- mindroom/tools/googlesearch.py +83 -0
- mindroom/tools/groq.py +77 -0
- mindroom/tools/hackernews.py +54 -0
- mindroom/tools/jina.py +108 -0
- mindroom/tools/jira.py +70 -0
- mindroom/tools/linear.py +103 -0
- mindroom/tools/linkup.py +65 -0
- mindroom/tools/lumalabs.py +71 -0
- mindroom/tools/mem0.py +82 -0
- mindroom/tools/modelslabs.py +85 -0
- mindroom/tools/moviepy_video_tools.py +62 -0
- mindroom/tools/newspaper4k.py +63 -0
- mindroom/tools/openai.py +143 -0
- mindroom/tools/openweather.py +89 -0
- mindroom/tools/oxylabs.py +54 -0
- mindroom/tools/pandas.py +35 -0
- mindroom/tools/pubmed.py +64 -0
- mindroom/tools/python.py +120 -0
- mindroom/tools/reddit.py +155 -0
- mindroom/tools/replicate.py +56 -0
- mindroom/tools/resend.py +55 -0
- mindroom/tools/scrapegraph.py +87 -0
- mindroom/tools/searxng.py +120 -0
- mindroom/tools/serpapi.py +55 -0
- mindroom/tools/serper.py +81 -0
- mindroom/tools/shell.py +46 -0
- mindroom/tools/slack.py +80 -0
- mindroom/tools/sleep.py +38 -0
- mindroom/tools/spider.py +62 -0
- mindroom/tools/sql.py +138 -0
- mindroom/tools/tavily.py +104 -0
- mindroom/tools/telegram.py +54 -0
- mindroom/tools/todoist.py +103 -0
- mindroom/tools/trello.py +121 -0
- mindroom/tools/twilio.py +97 -0
- mindroom/tools/web_browser_tools.py +37 -0
- mindroom/tools/webex.py +63 -0
- mindroom/tools/website.py +45 -0
- mindroom/tools/whatsapp.py +81 -0
- mindroom/tools/wikipedia.py +45 -0
- mindroom/tools/x.py +97 -0
- mindroom/tools/yfinance.py +121 -0
- mindroom/tools/youtube.py +81 -0
- mindroom/tools/zendesk.py +62 -0
- mindroom/tools/zep.py +107 -0
- mindroom/tools/zoom.py +62 -0
- mindroom/tools_metadata.json +7643 -0
- mindroom/tools_metadata.py +220 -0
- mindroom/topic_generator.py +153 -0
- mindroom/voice_handler.py +266 -0
- mindroom-0.1.1.dist-info/METADATA +425 -0
- mindroom-0.1.1.dist-info/RECORD +152 -0
- {mindroom-0.0.0.dist-info → mindroom-0.1.1.dist-info}/WHEEL +1 -2
- mindroom-0.1.1.dist-info/entry_points.txt +2 -0
- mindroom-0.0.0.dist-info/METADATA +0 -24
- mindroom-0.0.0.dist-info/RECORD +0 -4
- mindroom-0.0.0.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Unified credentials management API."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, HTTPException
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from mindroom.credentials import get_credentials_manager
|
|
9
|
+
|
|
10
|
+
router = APIRouter(prefix="/api/credentials", tags=["credentials"])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SetApiKeyRequest(BaseModel):
|
|
14
|
+
"""Request to set an API key."""
|
|
15
|
+
|
|
16
|
+
service: str
|
|
17
|
+
api_key: str
|
|
18
|
+
key_name: str = "api_key"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CredentialStatus(BaseModel):
|
|
22
|
+
"""Status of a service's credentials."""
|
|
23
|
+
|
|
24
|
+
service: str
|
|
25
|
+
has_credentials: bool
|
|
26
|
+
key_names: list[str] | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SetCredentialsRequest(BaseModel):
|
|
30
|
+
"""Request to set multiple credentials for a service."""
|
|
31
|
+
|
|
32
|
+
credentials: dict[str, Any] # Can be strings, booleans, numbers, etc.
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@router.get("/list")
|
|
36
|
+
async def list_services() -> list[str]:
|
|
37
|
+
"""List all services with stored credentials."""
|
|
38
|
+
manager = get_credentials_manager()
|
|
39
|
+
return manager.list_services()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@router.get("/{service}/status")
|
|
43
|
+
async def get_credential_status(service: str) -> CredentialStatus:
|
|
44
|
+
"""Get the status of credentials for a service."""
|
|
45
|
+
manager = get_credentials_manager()
|
|
46
|
+
credentials = manager.load_credentials(service)
|
|
47
|
+
|
|
48
|
+
if credentials:
|
|
49
|
+
return CredentialStatus(
|
|
50
|
+
service=service,
|
|
51
|
+
has_credentials=True,
|
|
52
|
+
key_names=list(credentials.keys()) if isinstance(credentials, dict) else None,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return CredentialStatus(service=service, has_credentials=False)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@router.post("/{service}")
|
|
59
|
+
async def set_credentials(service: str, request: SetCredentialsRequest) -> dict[str, str]:
|
|
60
|
+
"""Set multiple credentials for a service."""
|
|
61
|
+
manager = get_credentials_manager()
|
|
62
|
+
|
|
63
|
+
# Save all credentials for the service
|
|
64
|
+
manager.save_credentials(service, request.credentials)
|
|
65
|
+
|
|
66
|
+
return {"status": "success", "message": f"Credentials saved for {service}"}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@router.post("/{service}/api-key")
|
|
70
|
+
async def set_api_key(service: str, request: SetApiKeyRequest) -> dict[str, str]:
|
|
71
|
+
"""Set an API key for a service."""
|
|
72
|
+
if request.service != service:
|
|
73
|
+
raise HTTPException(status_code=400, detail="Service mismatch in request")
|
|
74
|
+
|
|
75
|
+
manager = get_credentials_manager()
|
|
76
|
+
manager.set_api_key(service, request.api_key, request.key_name)
|
|
77
|
+
|
|
78
|
+
return {"status": "success", "message": f"API key set for {service}"}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@router.get("/{service}/api-key")
|
|
82
|
+
async def get_api_key(service: str, key_name: str = "api_key") -> dict[str, Any]:
|
|
83
|
+
"""Get the API key for a service (returns only existence status for security)."""
|
|
84
|
+
manager = get_credentials_manager()
|
|
85
|
+
api_key = manager.get_api_key(service, key_name)
|
|
86
|
+
|
|
87
|
+
if api_key:
|
|
88
|
+
# Don't return the actual key for security
|
|
89
|
+
return {
|
|
90
|
+
"service": service,
|
|
91
|
+
"has_key": True,
|
|
92
|
+
"key_name": key_name,
|
|
93
|
+
# Return masked version
|
|
94
|
+
"masked_key": f"{api_key[:4]}...{api_key[-4:]}" if len(api_key) > 8 else "****",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {"service": service, "has_key": False, "key_name": key_name}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@router.get("/{service}")
|
|
101
|
+
async def get_credentials(service: str) -> dict[str, Any]:
|
|
102
|
+
"""Get credentials for a service (for editing)."""
|
|
103
|
+
manager = get_credentials_manager()
|
|
104
|
+
credentials = manager.load_credentials(service)
|
|
105
|
+
|
|
106
|
+
if not credentials:
|
|
107
|
+
return {"service": service, "credentials": {}}
|
|
108
|
+
|
|
109
|
+
return {"service": service, "credentials": credentials}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@router.delete("/{service}")
|
|
113
|
+
async def delete_credentials(service: str) -> dict[str, str]:
|
|
114
|
+
"""Delete all credentials for a service."""
|
|
115
|
+
manager = get_credentials_manager()
|
|
116
|
+
manager.delete_credentials(service)
|
|
117
|
+
|
|
118
|
+
return {"status": "success", "message": f"Credentials deleted for {service}"}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@router.post("/{service}/test")
|
|
122
|
+
async def test_credentials(service: str) -> dict[str, Any]:
|
|
123
|
+
"""Test if credentials are valid for a service."""
|
|
124
|
+
# This is a placeholder - actual testing would depend on the service
|
|
125
|
+
manager = get_credentials_manager()
|
|
126
|
+
credentials = manager.load_credentials(service)
|
|
127
|
+
|
|
128
|
+
if not credentials:
|
|
129
|
+
raise HTTPException(status_code=404, detail=f"No credentials found for {service}")
|
|
130
|
+
|
|
131
|
+
# For now, just check if credentials exist
|
|
132
|
+
# In the future, we could implement actual validation per service
|
|
133
|
+
return {
|
|
134
|
+
"service": service,
|
|
135
|
+
"status": "success",
|
|
136
|
+
"message": "Credentials exist (validation not implemented)",
|
|
137
|
+
}
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""Unified Google Integration for MindRoom.
|
|
2
|
+
|
|
3
|
+
This module provides a single, comprehensive Google OAuth integration supporting:
|
|
4
|
+
- Gmail (read, compose, modify)
|
|
5
|
+
- Google Calendar (events, scheduling)
|
|
6
|
+
- Google Drive (file access)
|
|
7
|
+
|
|
8
|
+
Replaces the previous fragmented gmail_config.py, google_auth.py, and google_setup_wizard.py
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
from urllib.parse import urlparse
|
|
15
|
+
|
|
16
|
+
import jwt
|
|
17
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
18
|
+
from fastapi.responses import RedirectResponse
|
|
19
|
+
from google.auth.transport.requests import Request as GoogleRequest
|
|
20
|
+
from google.oauth2.credentials import Credentials
|
|
21
|
+
from google_auth_oauthlib.flow import Flow # type: ignore[import-untyped]
|
|
22
|
+
from pydantic import BaseModel
|
|
23
|
+
|
|
24
|
+
from mindroom.credentials import CredentialsManager
|
|
25
|
+
|
|
26
|
+
router = APIRouter(prefix="/api/google", tags=["google-integration"])
|
|
27
|
+
|
|
28
|
+
# Initialize credentials manager
|
|
29
|
+
creds_manager = CredentialsManager()
|
|
30
|
+
|
|
31
|
+
# OAuth scopes for all Google services needed by MindRoom
|
|
32
|
+
SCOPES = [
|
|
33
|
+
# Gmail
|
|
34
|
+
"https://www.googleapis.com/auth/gmail.readonly",
|
|
35
|
+
"https://www.googleapis.com/auth/gmail.modify",
|
|
36
|
+
"https://www.googleapis.com/auth/gmail.compose",
|
|
37
|
+
# Calendar
|
|
38
|
+
"https://www.googleapis.com/auth/calendar",
|
|
39
|
+
# Sheets
|
|
40
|
+
"https://www.googleapis.com/auth/spreadsheets",
|
|
41
|
+
# Drive
|
|
42
|
+
"https://www.googleapis.com/auth/drive.file",
|
|
43
|
+
# User info
|
|
44
|
+
"openid",
|
|
45
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
46
|
+
"https://www.googleapis.com/auth/userinfo.profile",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
# Environment path for OAuth credentials
|
|
50
|
+
ENV_PATH = Path(__file__).parent.parent.parent.parent.parent / ".env"
|
|
51
|
+
|
|
52
|
+
# Get configuration from environment
|
|
53
|
+
BACKEND_PORT = os.getenv("BACKEND_PORT", "8765")
|
|
54
|
+
REDIRECT_URI = os.getenv("GOOGLE_REDIRECT_URI", f"http://localhost:{BACKEND_PORT}/api/google/callback")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class GoogleStatus(BaseModel):
|
|
58
|
+
"""Google integration status."""
|
|
59
|
+
|
|
60
|
+
connected: bool
|
|
61
|
+
email: str | None = None
|
|
62
|
+
services: list[str] = []
|
|
63
|
+
error: str | None = None
|
|
64
|
+
has_credentials: bool = False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class GoogleAuthUrl(BaseModel):
|
|
68
|
+
"""Google OAuth URL response."""
|
|
69
|
+
|
|
70
|
+
auth_url: str
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_oauth_credentials() -> dict[str, Any] | None:
|
|
74
|
+
"""Get OAuth credentials from environment variables."""
|
|
75
|
+
client_id = os.getenv("GOOGLE_CLIENT_ID")
|
|
76
|
+
client_secret = os.getenv("GOOGLE_CLIENT_SECRET")
|
|
77
|
+
|
|
78
|
+
if not client_id or not client_secret:
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
"web": {
|
|
83
|
+
"client_id": client_id,
|
|
84
|
+
"client_secret": client_secret,
|
|
85
|
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
|
86
|
+
"token_uri": "https://oauth2.googleapis.com/token",
|
|
87
|
+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
|
88
|
+
"redirect_uris": [REDIRECT_URI],
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_google_credentials() -> Credentials | None:
|
|
94
|
+
"""Get Google credentials from stored token."""
|
|
95
|
+
token_data = creds_manager.load_credentials("google")
|
|
96
|
+
if not token_data:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
creds = Credentials(
|
|
101
|
+
token=token_data.get("token"),
|
|
102
|
+
refresh_token=token_data.get("refresh_token"),
|
|
103
|
+
token_uri=token_data.get("token_uri"),
|
|
104
|
+
client_id=token_data.get("client_id"),
|
|
105
|
+
client_secret=token_data.get("client_secret"),
|
|
106
|
+
scopes=token_data.get("scopes", SCOPES),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Refresh token if expired
|
|
110
|
+
if creds and creds.expired and creds.refresh_token:
|
|
111
|
+
creds.refresh(GoogleRequest())
|
|
112
|
+
# Save refreshed credentials
|
|
113
|
+
save_credentials(creds)
|
|
114
|
+
except Exception:
|
|
115
|
+
return None
|
|
116
|
+
else:
|
|
117
|
+
return creds if creds and creds.valid else None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def save_credentials(creds: Credentials) -> None:
|
|
121
|
+
"""Save credentials using the unified credentials manager."""
|
|
122
|
+
# Full token with all scopes
|
|
123
|
+
token_data = {
|
|
124
|
+
"token": creds.token,
|
|
125
|
+
"refresh_token": creds.refresh_token,
|
|
126
|
+
"token_uri": creds.token_uri,
|
|
127
|
+
"client_id": creds.client_id,
|
|
128
|
+
"client_secret": creds.client_secret,
|
|
129
|
+
"scopes": creds.scopes,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# Add ID token if available for user info
|
|
133
|
+
if hasattr(creds, "_id_token") and creds._id_token:
|
|
134
|
+
token_data["_id_token"] = creds._id_token
|
|
135
|
+
|
|
136
|
+
# Save using credentials manager (handles backward compatibility)
|
|
137
|
+
creds_manager.save_credentials("google", token_data)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def save_env_credentials(client_id: str, client_secret: str, project_id: str | None = None) -> None:
|
|
141
|
+
"""Save OAuth credentials to .env file."""
|
|
142
|
+
env_lines = []
|
|
143
|
+
if ENV_PATH.exists():
|
|
144
|
+
with ENV_PATH.open() as f:
|
|
145
|
+
env_lines = f.readlines()
|
|
146
|
+
|
|
147
|
+
# Update or add credentials
|
|
148
|
+
# Use current environment variable for redirect URI to support multiple deployments
|
|
149
|
+
current_redirect_uri = os.getenv("GOOGLE_REDIRECT_URI", REDIRECT_URI)
|
|
150
|
+
env_vars = {
|
|
151
|
+
"GOOGLE_CLIENT_ID": client_id,
|
|
152
|
+
"GOOGLE_CLIENT_SECRET": client_secret,
|
|
153
|
+
"GOOGLE_PROJECT_ID": project_id or "mindroom-integration",
|
|
154
|
+
"GOOGLE_REDIRECT_URI": current_redirect_uri,
|
|
155
|
+
"BACKEND_PORT": BACKEND_PORT,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for key, value in env_vars.items():
|
|
159
|
+
found = False
|
|
160
|
+
for i, line in enumerate(env_lines):
|
|
161
|
+
if line.startswith(f"{key}="):
|
|
162
|
+
env_lines[i] = f"{key}={value}\n"
|
|
163
|
+
found = True
|
|
164
|
+
break
|
|
165
|
+
if not found:
|
|
166
|
+
env_lines.append(f"{key}={value}\n")
|
|
167
|
+
|
|
168
|
+
# Write back to .env file
|
|
169
|
+
with ENV_PATH.open("w") as f:
|
|
170
|
+
f.writelines(env_lines)
|
|
171
|
+
|
|
172
|
+
# Also set in current environment
|
|
173
|
+
for key, value in env_vars.items():
|
|
174
|
+
os.environ[key] = value
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@router.get("/status")
|
|
178
|
+
async def get_status() -> GoogleStatus:
|
|
179
|
+
"""Check Google integration status."""
|
|
180
|
+
# Check environment variables
|
|
181
|
+
client_id = os.getenv("GOOGLE_CLIENT_ID")
|
|
182
|
+
client_secret = os.getenv("GOOGLE_CLIENT_SECRET")
|
|
183
|
+
has_credentials = bool(client_id and client_secret)
|
|
184
|
+
|
|
185
|
+
# Get current credentials
|
|
186
|
+
creds = get_google_credentials()
|
|
187
|
+
|
|
188
|
+
if not creds:
|
|
189
|
+
return GoogleStatus(
|
|
190
|
+
connected=False,
|
|
191
|
+
has_credentials=has_credentials,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
# Check which services are accessible based on scopes
|
|
196
|
+
services = []
|
|
197
|
+
if creds.has_scopes(["https://www.googleapis.com/auth/gmail.modify"]):
|
|
198
|
+
services.append("Gmail")
|
|
199
|
+
if creds.has_scopes(["https://www.googleapis.com/auth/calendar"]):
|
|
200
|
+
services.append("Google Calendar")
|
|
201
|
+
if creds.has_scopes(["https://www.googleapis.com/auth/spreadsheets"]):
|
|
202
|
+
services.append("Google Sheets")
|
|
203
|
+
if creds.has_scopes(["https://www.googleapis.com/auth/drive.file"]):
|
|
204
|
+
services.append("Google Drive")
|
|
205
|
+
|
|
206
|
+
# Get user email from token
|
|
207
|
+
email = None
|
|
208
|
+
try:
|
|
209
|
+
if hasattr(creds, "_id_token") and creds._id_token:
|
|
210
|
+
decoded = jwt.decode(creds._id_token, options={"verify_signature": False})
|
|
211
|
+
email = decoded.get("email")
|
|
212
|
+
except Exception:
|
|
213
|
+
email = None
|
|
214
|
+
|
|
215
|
+
return GoogleStatus(
|
|
216
|
+
connected=True,
|
|
217
|
+
email=email,
|
|
218
|
+
services=services,
|
|
219
|
+
has_credentials=has_credentials,
|
|
220
|
+
)
|
|
221
|
+
except Exception as e:
|
|
222
|
+
return GoogleStatus(
|
|
223
|
+
connected=False,
|
|
224
|
+
error=str(e),
|
|
225
|
+
has_credentials=has_credentials,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@router.post("/connect")
|
|
230
|
+
async def connect() -> GoogleAuthUrl:
|
|
231
|
+
"""Start Google OAuth flow."""
|
|
232
|
+
oauth_config = get_oauth_credentials()
|
|
233
|
+
if not oauth_config:
|
|
234
|
+
raise HTTPException(
|
|
235
|
+
status_code=503,
|
|
236
|
+
detail="Google OAuth is not configured. Please set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET environment variables.",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
# Create OAuth flow with all scopes
|
|
241
|
+
# Use current environment variable for redirect URI to support multiple deployments
|
|
242
|
+
current_redirect_uri = os.getenv("GOOGLE_REDIRECT_URI", REDIRECT_URI)
|
|
243
|
+
flow = Flow.from_client_config(oauth_config, scopes=SCOPES, redirect_uri=current_redirect_uri)
|
|
244
|
+
|
|
245
|
+
# Generate authorization URL
|
|
246
|
+
auth_url, _ = flow.authorization_url(
|
|
247
|
+
access_type="offline",
|
|
248
|
+
include_granted_scopes="true",
|
|
249
|
+
prompt="consent",
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
return GoogleAuthUrl(auth_url=auth_url)
|
|
253
|
+
except Exception as e:
|
|
254
|
+
raise HTTPException(status_code=500, detail=f"Failed to start Google OAuth: {e!s}") from e
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@router.get("/callback")
|
|
258
|
+
async def callback(request: Request) -> RedirectResponse:
|
|
259
|
+
"""Handle Google OAuth callback."""
|
|
260
|
+
# Get the authorization code from the callback
|
|
261
|
+
code = request.query_params.get("code")
|
|
262
|
+
if not code:
|
|
263
|
+
raise HTTPException(status_code=400, detail="No authorization code received")
|
|
264
|
+
|
|
265
|
+
oauth_config = get_oauth_credentials()
|
|
266
|
+
if not oauth_config:
|
|
267
|
+
raise HTTPException(status_code=503, detail="OAuth not configured")
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
# Create OAuth flow and exchange code for tokens
|
|
271
|
+
# Use current environment variable for redirect URI to support multiple deployments
|
|
272
|
+
current_redirect_uri = os.getenv("GOOGLE_REDIRECT_URI", REDIRECT_URI)
|
|
273
|
+
flow = Flow.from_client_config(oauth_config, scopes=SCOPES, redirect_uri=current_redirect_uri)
|
|
274
|
+
flow.fetch_token(code=code)
|
|
275
|
+
|
|
276
|
+
# Save credentials
|
|
277
|
+
save_credentials(flow.credentials)
|
|
278
|
+
|
|
279
|
+
# Redirect back to widget with success message
|
|
280
|
+
# Extract the domain from the redirect URI for the final redirect
|
|
281
|
+
parsed_uri = urlparse(current_redirect_uri)
|
|
282
|
+
base_url = f"{parsed_uri.scheme}://{parsed_uri.netloc}"
|
|
283
|
+
return RedirectResponse(url=f"{base_url}/?google=connected")
|
|
284
|
+
except Exception as e:
|
|
285
|
+
# Check if it's a scope change error
|
|
286
|
+
error_msg = str(e)
|
|
287
|
+
if "Scope has changed" in error_msg:
|
|
288
|
+
raise HTTPException(
|
|
289
|
+
status_code=400,
|
|
290
|
+
detail=f"OAuth scope mismatch: {error_msg}. Please disconnect and reconnect to authorize with the new scopes.",
|
|
291
|
+
) from e
|
|
292
|
+
raise HTTPException(status_code=500, detail=f"OAuth callback failed: {error_msg}") from e
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@router.post("/disconnect")
|
|
296
|
+
async def disconnect() -> dict[str, str]:
|
|
297
|
+
"""Disconnect Google services by removing stored tokens."""
|
|
298
|
+
try:
|
|
299
|
+
# Remove credentials using the manager
|
|
300
|
+
creds_manager.delete_credentials("google")
|
|
301
|
+
except Exception as e:
|
|
302
|
+
raise HTTPException(status_code=500, detail=f"Failed to disconnect: {e!s}") from e
|
|
303
|
+
else:
|
|
304
|
+
return {"status": "disconnected"}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@router.post("/configure")
|
|
308
|
+
async def configure(credentials: dict[str, str]) -> dict[str, Any]:
|
|
309
|
+
"""Configure Google OAuth credentials manually."""
|
|
310
|
+
client_id = credentials.get("client_id")
|
|
311
|
+
client_secret = credentials.get("client_secret")
|
|
312
|
+
project_id = credentials.get("project_id", "mindroom-integration")
|
|
313
|
+
|
|
314
|
+
if not client_id or not client_secret:
|
|
315
|
+
raise HTTPException(
|
|
316
|
+
status_code=400,
|
|
317
|
+
detail="client_id and client_secret are required",
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
# Save to environment
|
|
322
|
+
save_env_credentials(client_id, client_secret, project_id)
|
|
323
|
+
except Exception as e:
|
|
324
|
+
raise HTTPException(status_code=500, detail=f"Failed to save credentials: {e!s}") from e
|
|
325
|
+
else:
|
|
326
|
+
return {"success": True, "message": "Google OAuth credentials configured successfully"}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@router.post("/reset")
|
|
330
|
+
async def reset() -> dict[str, Any]:
|
|
331
|
+
"""Reset Google integration by removing all credentials and tokens."""
|
|
332
|
+
try:
|
|
333
|
+
# Remove credentials using the manager
|
|
334
|
+
creds_manager.delete_credentials("google")
|
|
335
|
+
|
|
336
|
+
# Remove from environment variables
|
|
337
|
+
if ENV_PATH.exists():
|
|
338
|
+
with ENV_PATH.open() as f:
|
|
339
|
+
lines = f.readlines()
|
|
340
|
+
|
|
341
|
+
# Filter out Google-related variables
|
|
342
|
+
google_vars = [
|
|
343
|
+
"GOOGLE_CLIENT_ID",
|
|
344
|
+
"GOOGLE_CLIENT_SECRET",
|
|
345
|
+
"GOOGLE_PROJECT_ID",
|
|
346
|
+
"GOOGLE_REDIRECT_URI",
|
|
347
|
+
]
|
|
348
|
+
filtered_lines = [line for line in lines if not any(line.startswith(f"{var}=") for var in google_vars)]
|
|
349
|
+
|
|
350
|
+
with ENV_PATH.open("w") as f:
|
|
351
|
+
f.writelines(filtered_lines)
|
|
352
|
+
except Exception as e:
|
|
353
|
+
raise HTTPException(status_code=500, detail=f"Failed to reset: {e!s}") from e
|
|
354
|
+
else:
|
|
355
|
+
return {"success": True, "message": "Google integration reset successfully"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Helper utilities for Google tools management."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_google_tool_scopes(tool_name: str) -> list[str]:
|
|
7
|
+
"""Get required OAuth scopes for a Google tool."""
|
|
8
|
+
scope_map = {
|
|
9
|
+
"google_calendar": [
|
|
10
|
+
"https://www.googleapis.com/auth/calendar",
|
|
11
|
+
"https://www.googleapis.com/auth/calendar.readonly",
|
|
12
|
+
],
|
|
13
|
+
"google_sheets": [
|
|
14
|
+
"https://www.googleapis.com/auth/spreadsheets",
|
|
15
|
+
"https://www.googleapis.com/auth/spreadsheets.readonly",
|
|
16
|
+
],
|
|
17
|
+
"gmail": [
|
|
18
|
+
"https://www.googleapis.com/auth/gmail.modify",
|
|
19
|
+
"https://www.googleapis.com/auth/gmail.readonly",
|
|
20
|
+
"https://www.googleapis.com/auth/gmail.compose",
|
|
21
|
+
"https://www.googleapis.com/auth/gmail.send",
|
|
22
|
+
],
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return scope_map.get(tool_name, [])
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def check_google_tool_configured(tool_name: str, google_creds: dict[str, Any]) -> bool:
|
|
29
|
+
"""Check if a Google tool has the required OAuth scopes configured."""
|
|
30
|
+
if not google_creds or "token" not in google_creds:
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
configured_scopes = google_creds.get("scopes", [])
|
|
34
|
+
required_scopes = get_google_tool_scopes(tool_name)
|
|
35
|
+
|
|
36
|
+
if not required_scopes:
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
# Check if any of the required scopes are present
|
|
40
|
+
return any(scope in configured_scopes for scope in required_scopes)
|