workspace-mcp 0.2.0__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.
- auth/__init__.py +1 -0
- auth/google_auth.py +549 -0
- auth/oauth_callback_server.py +241 -0
- auth/oauth_responses.py +223 -0
- auth/scopes.py +108 -0
- auth/service_decorator.py +404 -0
- core/__init__.py +1 -0
- core/server.py +214 -0
- core/utils.py +162 -0
- gcalendar/__init__.py +1 -0
- gcalendar/calendar_tools.py +496 -0
- gchat/__init__.py +6 -0
- gchat/chat_tools.py +254 -0
- gdocs/__init__.py +0 -0
- gdocs/docs_tools.py +244 -0
- gdrive/__init__.py +0 -0
- gdrive/drive_tools.py +362 -0
- gforms/__init__.py +3 -0
- gforms/forms_tools.py +318 -0
- gmail/__init__.py +1 -0
- gmail/gmail_tools.py +807 -0
- gsheets/__init__.py +23 -0
- gsheets/sheets_tools.py +393 -0
- gslides/__init__.py +0 -0
- gslides/slides_tools.py +316 -0
- main.py +160 -0
- workspace_mcp-0.2.0.dist-info/METADATA +29 -0
- workspace_mcp-0.2.0.dist-info/RECORD +32 -0
- workspace_mcp-0.2.0.dist-info/WHEEL +5 -0
- workspace_mcp-0.2.0.dist-info/entry_points.txt +2 -0
- workspace_mcp-0.2.0.dist-info/licenses/LICENSE +21 -0
- workspace_mcp-0.2.0.dist-info/top_level.txt +11 -0
gchat/chat_tools.py
ADDED
@@ -0,0 +1,254 @@
|
|
1
|
+
"""
|
2
|
+
Google Chat MCP Tools
|
3
|
+
|
4
|
+
This module provides MCP tools for interacting with Google Chat API.
|
5
|
+
"""
|
6
|
+
import logging
|
7
|
+
import asyncio
|
8
|
+
from typing import Optional
|
9
|
+
|
10
|
+
from mcp import types
|
11
|
+
from googleapiclient.errors import HttpError
|
12
|
+
|
13
|
+
# Auth & server utilities
|
14
|
+
from auth.service_decorator import require_google_service
|
15
|
+
from core.server import server
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
@server.tool()
|
20
|
+
@require_google_service("chat", "chat_read")
|
21
|
+
async def list_spaces(
|
22
|
+
service,
|
23
|
+
user_google_email: str,
|
24
|
+
page_size: int = 100,
|
25
|
+
space_type: str = "all" # "all", "room", "dm"
|
26
|
+
) -> str:
|
27
|
+
"""
|
28
|
+
Lists Google Chat spaces (rooms and direct messages) accessible to the user.
|
29
|
+
|
30
|
+
Returns:
|
31
|
+
str: A formatted list of Google Chat spaces accessible to the user.
|
32
|
+
"""
|
33
|
+
logger.info(f"[list_spaces] Email={user_google_email}, Type={space_type}")
|
34
|
+
|
35
|
+
try:
|
36
|
+
# Build filter based on space_type
|
37
|
+
filter_param = None
|
38
|
+
if space_type == "room":
|
39
|
+
filter_param = "spaceType = SPACE"
|
40
|
+
elif space_type == "dm":
|
41
|
+
filter_param = "spaceType = DIRECT_MESSAGE"
|
42
|
+
|
43
|
+
request_params = {"pageSize": page_size}
|
44
|
+
if filter_param:
|
45
|
+
request_params["filter"] = filter_param
|
46
|
+
|
47
|
+
response = await asyncio.to_thread(
|
48
|
+
service.spaces().list(**request_params).execute
|
49
|
+
)
|
50
|
+
|
51
|
+
spaces = response.get('spaces', [])
|
52
|
+
if not spaces:
|
53
|
+
return f"No Chat spaces found for type '{space_type}'."
|
54
|
+
|
55
|
+
output = [f"Found {len(spaces)} Chat spaces (type: {space_type}):"]
|
56
|
+
for space in spaces:
|
57
|
+
space_name = space.get('displayName', 'Unnamed Space')
|
58
|
+
space_id = space.get('name', '')
|
59
|
+
space_type_actual = space.get('spaceType', 'UNKNOWN')
|
60
|
+
output.append(f"- {space_name} (ID: {space_id}, Type: {space_type_actual})")
|
61
|
+
|
62
|
+
return "\n".join(output)
|
63
|
+
|
64
|
+
except HttpError as e:
|
65
|
+
logger.error(f"API error in list_spaces: {e}", exc_info=True)
|
66
|
+
raise Exception(f"API error: {e}")
|
67
|
+
except Exception as e:
|
68
|
+
logger.exception(f"Unexpected error in list_spaces: {e}")
|
69
|
+
raise Exception(f"Unexpected error: {e}")
|
70
|
+
|
71
|
+
@server.tool()
|
72
|
+
@require_google_service("chat", "chat_read")
|
73
|
+
async def get_messages(
|
74
|
+
service,
|
75
|
+
user_google_email: str,
|
76
|
+
space_id: str,
|
77
|
+
page_size: int = 50,
|
78
|
+
order_by: str = "createTime desc"
|
79
|
+
) -> str:
|
80
|
+
"""
|
81
|
+
Retrieves messages from a Google Chat space.
|
82
|
+
|
83
|
+
Returns:
|
84
|
+
str: Formatted messages from the specified space.
|
85
|
+
"""
|
86
|
+
logger.info(f"[get_messages] Space ID: '{space_id}' for user '{user_google_email}'")
|
87
|
+
|
88
|
+
try:
|
89
|
+
# Get space info first
|
90
|
+
space_info = await asyncio.to_thread(
|
91
|
+
service.spaces().get(name=space_id).execute
|
92
|
+
)
|
93
|
+
space_name = space_info.get('displayName', 'Unknown Space')
|
94
|
+
|
95
|
+
# Get messages
|
96
|
+
response = await asyncio.to_thread(
|
97
|
+
service.spaces().messages().list(
|
98
|
+
parent=space_id,
|
99
|
+
pageSize=page_size,
|
100
|
+
orderBy=order_by
|
101
|
+
).execute
|
102
|
+
)
|
103
|
+
|
104
|
+
messages = response.get('messages', [])
|
105
|
+
if not messages:
|
106
|
+
return f"No messages found in space '{space_name}' (ID: {space_id})."
|
107
|
+
|
108
|
+
output = [f"Messages from '{space_name}' (ID: {space_id}):\n"]
|
109
|
+
for msg in messages:
|
110
|
+
sender = msg.get('sender', {}).get('displayName', 'Unknown Sender')
|
111
|
+
create_time = msg.get('createTime', 'Unknown Time')
|
112
|
+
text_content = msg.get('text', 'No text content')
|
113
|
+
msg_name = msg.get('name', '')
|
114
|
+
|
115
|
+
output.append(f"[{create_time}] {sender}:")
|
116
|
+
output.append(f" {text_content}")
|
117
|
+
output.append(f" (Message ID: {msg_name})\n")
|
118
|
+
|
119
|
+
return "\n".join(output)
|
120
|
+
|
121
|
+
except HttpError as error:
|
122
|
+
logger.error(f"[get_messages] API error for space {space_id}: {error}", exc_info=True)
|
123
|
+
raise Exception(f"API error accessing space {space_id}: {error}")
|
124
|
+
except Exception as e:
|
125
|
+
logger.exception(f"[get_messages] Unexpected error for space {space_id}: {e}")
|
126
|
+
raise Exception(f"Unexpected error accessing space {space_id}: {e}")
|
127
|
+
|
128
|
+
@server.tool()
|
129
|
+
@require_google_service("chat", "chat_write")
|
130
|
+
async def send_message(
|
131
|
+
service,
|
132
|
+
user_google_email: str,
|
133
|
+
space_id: str,
|
134
|
+
message_text: str,
|
135
|
+
thread_key: Optional[str] = None
|
136
|
+
) -> str:
|
137
|
+
"""
|
138
|
+
Sends a message to a Google Chat space.
|
139
|
+
|
140
|
+
Returns:
|
141
|
+
str: Confirmation message with sent message details.
|
142
|
+
"""
|
143
|
+
logger.info(f"[send_message] Email: '{user_google_email}', Space: '{space_id}'")
|
144
|
+
|
145
|
+
try:
|
146
|
+
message_body = {
|
147
|
+
'text': message_text
|
148
|
+
}
|
149
|
+
|
150
|
+
# Add thread key if provided (for threaded replies)
|
151
|
+
request_params = {
|
152
|
+
'parent': space_id,
|
153
|
+
'body': message_body
|
154
|
+
}
|
155
|
+
if thread_key:
|
156
|
+
request_params['threadKey'] = thread_key
|
157
|
+
|
158
|
+
message = await asyncio.to_thread(
|
159
|
+
service.spaces().messages().create(**request_params).execute
|
160
|
+
)
|
161
|
+
|
162
|
+
message_name = message.get('name', '')
|
163
|
+
create_time = message.get('createTime', '')
|
164
|
+
|
165
|
+
msg = f"Message sent to space '{space_id}' by {user_google_email}. Message ID: {message_name}, Time: {create_time}"
|
166
|
+
logger.info(f"Successfully sent message to space '{space_id}' by {user_google_email}")
|
167
|
+
return msg
|
168
|
+
|
169
|
+
except HttpError as e:
|
170
|
+
logger.error(f"API error in send_message: {e}", exc_info=True)
|
171
|
+
raise Exception(f"API error: {e}")
|
172
|
+
except Exception as e:
|
173
|
+
logger.exception(f"Unexpected error in send_message: {e}")
|
174
|
+
raise Exception(f"Unexpected error: {e}")
|
175
|
+
|
176
|
+
@server.tool()
|
177
|
+
@require_google_service("chat", "chat_read")
|
178
|
+
async def search_messages(
|
179
|
+
service,
|
180
|
+
user_google_email: str,
|
181
|
+
query: str,
|
182
|
+
space_id: Optional[str] = None,
|
183
|
+
page_size: int = 25
|
184
|
+
) -> str:
|
185
|
+
"""
|
186
|
+
Searches for messages in Google Chat spaces by text content.
|
187
|
+
|
188
|
+
Returns:
|
189
|
+
str: A formatted list of messages matching the search query.
|
190
|
+
"""
|
191
|
+
logger.info(f"[search_messages] Email={user_google_email}, Query='{query}'")
|
192
|
+
|
193
|
+
try:
|
194
|
+
# If specific space provided, search within that space
|
195
|
+
if space_id:
|
196
|
+
response = await asyncio.to_thread(
|
197
|
+
service.spaces().messages().list(
|
198
|
+
parent=space_id,
|
199
|
+
pageSize=page_size,
|
200
|
+
filter=f'text:"{query}"'
|
201
|
+
).execute
|
202
|
+
)
|
203
|
+
messages = response.get('messages', [])
|
204
|
+
context = f"space '{space_id}'"
|
205
|
+
else:
|
206
|
+
# Search across all accessible spaces (this may require iterating through spaces)
|
207
|
+
# For simplicity, we'll search the user's spaces first
|
208
|
+
spaces_response = await asyncio.to_thread(
|
209
|
+
service.spaces().list(pageSize=100).execute
|
210
|
+
)
|
211
|
+
spaces = spaces_response.get('spaces', [])
|
212
|
+
|
213
|
+
messages = []
|
214
|
+
for space in spaces[:10]: # Limit to first 10 spaces to avoid timeout
|
215
|
+
try:
|
216
|
+
space_messages = await asyncio.to_thread(
|
217
|
+
service.spaces().messages().list(
|
218
|
+
parent=space.get('name'),
|
219
|
+
pageSize=5,
|
220
|
+
filter=f'text:"{query}"'
|
221
|
+
).execute
|
222
|
+
)
|
223
|
+
space_msgs = space_messages.get('messages', [])
|
224
|
+
for msg in space_msgs:
|
225
|
+
msg['_space_name'] = space.get('displayName', 'Unknown')
|
226
|
+
messages.extend(space_msgs)
|
227
|
+
except HttpError:
|
228
|
+
continue # Skip spaces we can't access
|
229
|
+
context = "all accessible spaces"
|
230
|
+
|
231
|
+
if not messages:
|
232
|
+
return f"No messages found matching '{query}' in {context}."
|
233
|
+
|
234
|
+
output = [f"Found {len(messages)} messages matching '{query}' in {context}:"]
|
235
|
+
for msg in messages:
|
236
|
+
sender = msg.get('sender', {}).get('displayName', 'Unknown Sender')
|
237
|
+
create_time = msg.get('createTime', 'Unknown Time')
|
238
|
+
text_content = msg.get('text', 'No text content')
|
239
|
+
space_name = msg.get('_space_name', 'Unknown Space')
|
240
|
+
|
241
|
+
# Truncate long messages
|
242
|
+
if len(text_content) > 100:
|
243
|
+
text_content = text_content[:100] + "..."
|
244
|
+
|
245
|
+
output.append(f"- [{create_time}] {sender} in '{space_name}': {text_content}")
|
246
|
+
|
247
|
+
return "\n".join(output)
|
248
|
+
|
249
|
+
except HttpError as e:
|
250
|
+
logger.error(f"API error in search_messages: {e}", exc_info=True)
|
251
|
+
raise Exception(f"API error: {e}")
|
252
|
+
except Exception as e:
|
253
|
+
logger.exception(f"Unexpected error in search_messages: {e}")
|
254
|
+
raise Exception(f"Unexpected error: {e}")
|
gdocs/__init__.py
ADDED
File without changes
|
gdocs/docs_tools.py
ADDED
@@ -0,0 +1,244 @@
|
|
1
|
+
"""
|
2
|
+
Google Docs MCP Tools
|
3
|
+
|
4
|
+
This module provides MCP tools for interacting with Google Docs API and managing Google Docs via Drive.
|
5
|
+
"""
|
6
|
+
import logging
|
7
|
+
import asyncio
|
8
|
+
import io
|
9
|
+
from typing import List
|
10
|
+
|
11
|
+
from mcp import types
|
12
|
+
from googleapiclient.errors import HttpError
|
13
|
+
from googleapiclient.http import MediaIoBaseDownload
|
14
|
+
|
15
|
+
# Auth & server utilities
|
16
|
+
from auth.service_decorator import require_google_service, require_multiple_services
|
17
|
+
from core.utils import extract_office_xml_text
|
18
|
+
from core.server import server
|
19
|
+
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
22
|
+
@server.tool()
|
23
|
+
@require_google_service("drive", "drive_read")
|
24
|
+
async def search_docs(
|
25
|
+
service,
|
26
|
+
user_google_email: str,
|
27
|
+
query: str,
|
28
|
+
page_size: int = 10,
|
29
|
+
) -> str:
|
30
|
+
"""
|
31
|
+
Searches for Google Docs by name using Drive API (mimeType filter).
|
32
|
+
|
33
|
+
Returns:
|
34
|
+
str: A formatted list of Google Docs matching the search query.
|
35
|
+
"""
|
36
|
+
logger.info(f"[search_docs] Email={user_google_email}, Query='{query}'")
|
37
|
+
|
38
|
+
try:
|
39
|
+
escaped_query = query.replace("'", "\\'")
|
40
|
+
|
41
|
+
response = await asyncio.to_thread(
|
42
|
+
service.files().list(
|
43
|
+
q=f"name contains '{escaped_query}' and mimeType='application/vnd.google-apps.document' and trashed=false",
|
44
|
+
pageSize=page_size,
|
45
|
+
fields="files(id, name, createdTime, modifiedTime, webViewLink)"
|
46
|
+
).execute
|
47
|
+
)
|
48
|
+
files = response.get('files', [])
|
49
|
+
if not files:
|
50
|
+
return f"No Google Docs found matching '{query}'."
|
51
|
+
|
52
|
+
output = [f"Found {len(files)} Google Docs matching '{query}':"]
|
53
|
+
for f in files:
|
54
|
+
output.append(
|
55
|
+
f"- {f['name']} (ID: {f['id']}) Modified: {f.get('modifiedTime')} Link: {f.get('webViewLink')}"
|
56
|
+
)
|
57
|
+
return "\n".join(output)
|
58
|
+
|
59
|
+
except HttpError as e:
|
60
|
+
logger.error(f"API error in search_docs: {e}", exc_info=True)
|
61
|
+
raise Exception(f"API error: {e}")
|
62
|
+
|
63
|
+
@server.tool()
|
64
|
+
@require_multiple_services([
|
65
|
+
{"service_type": "drive", "scopes": "drive_read", "param_name": "drive_service"},
|
66
|
+
{"service_type": "docs", "scopes": "docs_read", "param_name": "docs_service"}
|
67
|
+
])
|
68
|
+
async def get_doc_content(
|
69
|
+
drive_service,
|
70
|
+
docs_service,
|
71
|
+
user_google_email: str,
|
72
|
+
document_id: str,
|
73
|
+
) -> str:
|
74
|
+
"""
|
75
|
+
Retrieves content of a Google Doc or a Drive file (like .docx) identified by document_id.
|
76
|
+
- Native Google Docs: Fetches content via Docs API.
|
77
|
+
- Office files (.docx, etc.) stored in Drive: Downloads via Drive API and extracts text.
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
str: The document content with metadata header.
|
81
|
+
"""
|
82
|
+
logger.info(f"[get_doc_content] Invoked. Document/File ID: '{document_id}' for user '{user_google_email}'")
|
83
|
+
|
84
|
+
try:
|
85
|
+
# Step 2: Get file metadata from Drive
|
86
|
+
file_metadata = await asyncio.to_thread(
|
87
|
+
drive_service.files().get(
|
88
|
+
fileId=document_id, fields="id, name, mimeType, webViewLink"
|
89
|
+
).execute
|
90
|
+
)
|
91
|
+
mime_type = file_metadata.get("mimeType", "")
|
92
|
+
file_name = file_metadata.get("name", "Unknown File")
|
93
|
+
web_view_link = file_metadata.get("webViewLink", "#")
|
94
|
+
|
95
|
+
logger.info(f"[get_doc_content] File '{file_name}' (ID: {document_id}) has mimeType: '{mime_type}'")
|
96
|
+
|
97
|
+
body_text = "" # Initialize body_text
|
98
|
+
|
99
|
+
# Step 3: Process based on mimeType
|
100
|
+
if mime_type == "application/vnd.google-apps.document":
|
101
|
+
logger.info(f"[get_doc_content] Processing as native Google Doc.")
|
102
|
+
doc_data = await asyncio.to_thread(
|
103
|
+
docs_service.documents().get(documentId=document_id).execute
|
104
|
+
)
|
105
|
+
body_elements = doc_data.get('body', {}).get('content', [])
|
106
|
+
|
107
|
+
processed_text_lines: List[str] = []
|
108
|
+
for element in body_elements:
|
109
|
+
if 'paragraph' in element:
|
110
|
+
paragraph = element.get('paragraph', {})
|
111
|
+
para_elements = paragraph.get('elements', [])
|
112
|
+
current_line_text = ""
|
113
|
+
for pe in para_elements:
|
114
|
+
text_run = pe.get('textRun', {})
|
115
|
+
if text_run and 'content' in text_run:
|
116
|
+
current_line_text += text_run['content']
|
117
|
+
if current_line_text.strip():
|
118
|
+
processed_text_lines.append(current_line_text)
|
119
|
+
body_text = "".join(processed_text_lines)
|
120
|
+
else:
|
121
|
+
logger.info(f"[get_doc_content] Processing as Drive file (e.g., .docx, other). MimeType: {mime_type}")
|
122
|
+
|
123
|
+
export_mime_type_map = {
|
124
|
+
# Example: "application/vnd.google-apps.spreadsheet"z: "text/csv",
|
125
|
+
# Native GSuite types that are not Docs would go here if this function
|
126
|
+
# was intended to export them. For .docx, direct download is used.
|
127
|
+
}
|
128
|
+
effective_export_mime = export_mime_type_map.get(mime_type)
|
129
|
+
|
130
|
+
request_obj = (
|
131
|
+
drive_service.files().export_media(fileId=document_id, mimeType=effective_export_mime)
|
132
|
+
if effective_export_mime
|
133
|
+
else drive_service.files().get_media(fileId=document_id)
|
134
|
+
)
|
135
|
+
|
136
|
+
fh = io.BytesIO()
|
137
|
+
downloader = MediaIoBaseDownload(fh, request_obj)
|
138
|
+
loop = asyncio.get_event_loop()
|
139
|
+
done = False
|
140
|
+
while not done:
|
141
|
+
status, done = await loop.run_in_executor(None, downloader.next_chunk)
|
142
|
+
|
143
|
+
file_content_bytes = fh.getvalue()
|
144
|
+
|
145
|
+
office_text = extract_office_xml_text(file_content_bytes, mime_type)
|
146
|
+
if office_text:
|
147
|
+
body_text = office_text
|
148
|
+
else:
|
149
|
+
try:
|
150
|
+
body_text = file_content_bytes.decode("utf-8")
|
151
|
+
except UnicodeDecodeError:
|
152
|
+
body_text = (
|
153
|
+
f"[Binary or unsupported text encoding for mimeType '{mime_type}' - "
|
154
|
+
f"{len(file_content_bytes)} bytes]"
|
155
|
+
)
|
156
|
+
|
157
|
+
header = (
|
158
|
+
f'File: "{file_name}" (ID: {document_id}, Type: {mime_type})\n'
|
159
|
+
f'Link: {web_view_link}\n\n--- CONTENT ---\n'
|
160
|
+
)
|
161
|
+
return header + body_text
|
162
|
+
|
163
|
+
except HttpError as error:
|
164
|
+
logger.error(
|
165
|
+
f"[get_doc_content] API error for ID {document_id}: {error}",
|
166
|
+
exc_info=True,
|
167
|
+
)
|
168
|
+
raise Exception(f"API error processing document/file ID {document_id}: {error}")
|
169
|
+
except Exception as e:
|
170
|
+
logger.exception(f"[get_doc_content] Unexpected error for ID {document_id}: {e}")
|
171
|
+
raise Exception(f"Unexpected error processing document/file ID {document_id}: {e}")
|
172
|
+
|
173
|
+
@server.tool()
|
174
|
+
@require_google_service("drive", "drive_read")
|
175
|
+
async def list_docs_in_folder(
|
176
|
+
service,
|
177
|
+
user_google_email: str,
|
178
|
+
folder_id: str = 'root',
|
179
|
+
page_size: int = 100
|
180
|
+
) -> str:
|
181
|
+
"""
|
182
|
+
Lists Google Docs within a specific Drive folder.
|
183
|
+
|
184
|
+
Returns:
|
185
|
+
str: A formatted list of Google Docs in the specified folder.
|
186
|
+
"""
|
187
|
+
logger.info(f"[list_docs_in_folder] Invoked. Email: '{user_google_email}', Folder ID: '{folder_id}'")
|
188
|
+
|
189
|
+
try:
|
190
|
+
rsp = await asyncio.to_thread(
|
191
|
+
service.files().list(
|
192
|
+
q=f"'{folder_id}' in parents and mimeType='application/vnd.google-apps.document' and trashed=false",
|
193
|
+
pageSize=page_size,
|
194
|
+
fields="files(id, name, modifiedTime, webViewLink)"
|
195
|
+
).execute
|
196
|
+
)
|
197
|
+
items = rsp.get('files', [])
|
198
|
+
if not items:
|
199
|
+
return f"No Google Docs found in folder '{folder_id}'."
|
200
|
+
out = [f"Found {len(items)} Docs in folder '{folder_id}':"]
|
201
|
+
for f in items:
|
202
|
+
out.append(f"- {f['name']} (ID: {f['id']}) Modified: {f.get('modifiedTime')} Link: {f.get('webViewLink')}")
|
203
|
+
return "\n".join(out)
|
204
|
+
|
205
|
+
except HttpError as e:
|
206
|
+
logger.error(f"API error in list_docs_in_folder: {e}", exc_info=True)
|
207
|
+
raise Exception(f"API error: {e}")
|
208
|
+
except Exception as e:
|
209
|
+
logger.exception(f"Unexpected error in list_docs_in_folder: {e}")
|
210
|
+
raise Exception(f"Unexpected error: {e}")
|
211
|
+
|
212
|
+
@server.tool()
|
213
|
+
@require_google_service("docs", "docs_write")
|
214
|
+
async def create_doc(
|
215
|
+
service,
|
216
|
+
user_google_email: str, # Made user_google_email required
|
217
|
+
title: str,
|
218
|
+
content: str = '',
|
219
|
+
) -> str:
|
220
|
+
"""
|
221
|
+
Creates a new Google Doc and optionally inserts initial content.
|
222
|
+
|
223
|
+
Returns:
|
224
|
+
str: Confirmation message with document ID and link.
|
225
|
+
"""
|
226
|
+
logger.info(f"[create_doc] Invoked. Email: '{user_google_email}', Title='{title}'")
|
227
|
+
|
228
|
+
try:
|
229
|
+
doc = await asyncio.to_thread(service.documents().create(body={'title': title}).execute)
|
230
|
+
doc_id = doc.get('documentId')
|
231
|
+
if content:
|
232
|
+
requests = [{'insertText': {'location': {'index': 1}, 'text': content}}]
|
233
|
+
await asyncio.to_thread(service.documents().batchUpdate(documentId=doc_id, body={'requests': requests}).execute)
|
234
|
+
link = f"https://docs.google.com/document/d/{doc_id}/edit"
|
235
|
+
msg = f"Created Google Doc '{title}' (ID: {doc_id}) for {user_google_email}. Link: {link}"
|
236
|
+
logger.info(f"Successfully created Google Doc '{title}' (ID: {doc_id}) for {user_google_email}. Link: {link}")
|
237
|
+
return msg
|
238
|
+
|
239
|
+
except HttpError as e:
|
240
|
+
logger.error(f"API error in create_doc: {e}", exc_info=True)
|
241
|
+
raise Exception(f"API error: {e}")
|
242
|
+
except Exception as e:
|
243
|
+
logger.exception(f"Unexpected error in create_doc: {e}")
|
244
|
+
raise Exception(f"Unexpected error: {e}")
|
gdrive/__init__.py
ADDED
File without changes
|