pygpt-net 2.6.1__py3-none-any.whl → 2.6.2__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.
- pygpt_net/CHANGELOG.txt +4 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +15 -1
- pygpt_net/controller/chat/response.py +5 -3
- pygpt_net/controller/chat/stream.py +40 -2
- pygpt_net/controller/plugins/plugins.py +25 -0
- pygpt_net/controller/presets/editor.py +33 -88
- pygpt_net/controller/presets/experts.py +20 -1
- pygpt_net/controller/presets/presets.py +2 -2
- pygpt_net/controller/ui/mode.py +17 -66
- pygpt_net/core/agents/runner.py +15 -7
- pygpt_net/core/experts/experts.py +3 -3
- pygpt_net/data/config/config.json +3 -3
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/locale/locale.de.ini +2 -0
- pygpt_net/data/locale/locale.en.ini +2 -0
- pygpt_net/data/locale/locale.es.ini +2 -0
- pygpt_net/data/locale/locale.fr.ini +2 -0
- pygpt_net/data/locale/locale.it.ini +2 -0
- pygpt_net/data/locale/locale.pl.ini +3 -1
- pygpt_net/data/locale/locale.uk.ini +2 -0
- pygpt_net/data/locale/locale.zh.ini +2 -0
- pygpt_net/plugin/base/plugin.py +35 -3
- pygpt_net/plugin/bitbucket/__init__.py +12 -0
- pygpt_net/plugin/bitbucket/config.py +267 -0
- pygpt_net/plugin/bitbucket/plugin.py +125 -0
- pygpt_net/plugin/bitbucket/worker.py +569 -0
- pygpt_net/plugin/facebook/__init__.py +12 -0
- pygpt_net/plugin/facebook/config.py +359 -0
- pygpt_net/plugin/facebook/plugin.py +114 -0
- pygpt_net/plugin/facebook/worker.py +698 -0
- pygpt_net/plugin/github/__init__.py +12 -0
- pygpt_net/plugin/github/config.py +441 -0
- pygpt_net/plugin/github/plugin.py +124 -0
- pygpt_net/plugin/github/worker.py +674 -0
- pygpt_net/plugin/google/__init__.py +12 -0
- pygpt_net/plugin/google/config.py +367 -0
- pygpt_net/plugin/google/plugin.py +126 -0
- pygpt_net/plugin/google/worker.py +826 -0
- pygpt_net/plugin/slack/__init__.py +12 -0
- pygpt_net/plugin/slack/config.py +349 -0
- pygpt_net/plugin/slack/plugin.py +116 -0
- pygpt_net/plugin/slack/worker.py +639 -0
- pygpt_net/plugin/telegram/__init__.py +12 -0
- pygpt_net/plugin/telegram/config.py +308 -0
- pygpt_net/plugin/telegram/plugin.py +118 -0
- pygpt_net/plugin/telegram/worker.py +563 -0
- pygpt_net/plugin/twitter/__init__.py +12 -0
- pygpt_net/plugin/twitter/config.py +491 -0
- pygpt_net/plugin/twitter/plugin.py +126 -0
- pygpt_net/plugin/twitter/worker.py +837 -0
- pygpt_net/provider/agents/llama_index/legacy/openai_assistant.py +35 -3
- pygpt_net/ui/base/config_dialog.py +4 -0
- pygpt_net/ui/dialog/preset.py +34 -77
- pygpt_net/ui/layout/toolbox/presets.py +2 -2
- pygpt_net/ui/main.py +3 -1
- {pygpt_net-2.6.1.dist-info → pygpt_net-2.6.2.dist-info}/METADATA +145 -2
- {pygpt_net-2.6.1.dist-info → pygpt_net-2.6.2.dist-info}/RECORD +61 -33
- {pygpt_net-2.6.1.dist-info → pygpt_net-2.6.2.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.1.dist-info → pygpt_net-2.6.2.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.1.dist-info → pygpt_net-2.6.2.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,826 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# ================================================== #
|
|
4
|
+
# This file is a part of PYGPT package #
|
|
5
|
+
# Website: https://pygpt.net #
|
|
6
|
+
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
|
+
# MIT License #
|
|
8
|
+
# Created By : Marcin Szczygliński #
|
|
9
|
+
# Updated Date: 2025.08.15 00:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import base64
|
|
15
|
+
import datetime as dt
|
|
16
|
+
import io
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
from email.message import EmailMessage
|
|
21
|
+
from email.mime.base import MIMEBase
|
|
22
|
+
from email.mime.multipart import MIMEMultipart
|
|
23
|
+
from email.mime.text import MIMEText
|
|
24
|
+
from email import encoders
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
27
|
+
|
|
28
|
+
from PySide6.QtCore import Slot
|
|
29
|
+
|
|
30
|
+
from pygpt_net.plugin.base.worker import BaseWorker, BaseSignals
|
|
31
|
+
|
|
32
|
+
# Google libs
|
|
33
|
+
from googleapiclient.discovery import build
|
|
34
|
+
from googleapiclient.errors import HttpError
|
|
35
|
+
from googleapiclient.http import MediaIoBaseDownload, MediaFileUpload
|
|
36
|
+
from google.oauth2.credentials import Credentials
|
|
37
|
+
from google.oauth2.service_account import Credentials as ServiceAccountCredentials
|
|
38
|
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
39
|
+
from google.auth.transport.requests import Request
|
|
40
|
+
|
|
41
|
+
# Optional libs
|
|
42
|
+
try:
|
|
43
|
+
from youtube_transcript_api import YouTubeTranscriptApi # unofficial fallback
|
|
44
|
+
except Exception:
|
|
45
|
+
YouTubeTranscriptApi = None
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
import gkeepapi # unofficial Keep fallback
|
|
49
|
+
except Exception:
|
|
50
|
+
gkeepapi = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class WorkerSignals(BaseSignals):
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Worker(BaseWorker):
|
|
58
|
+
"""
|
|
59
|
+
Google plugin worker: Gmail, Calendar, Keep, Drive, YouTube, Contacts.
|
|
60
|
+
"""
|
|
61
|
+
# Request a broad-but-reasonable union of scopes once, to avoid re-prompt churn
|
|
62
|
+
GMAIL_SCOPES = [
|
|
63
|
+
"https://www.googleapis.com/auth/gmail.readonly",
|
|
64
|
+
"https://www.googleapis.com/auth/gmail.send",
|
|
65
|
+
"https://www.googleapis.com/auth/gmail.modify",
|
|
66
|
+
]
|
|
67
|
+
CAL_SCOPES = ["https://www.googleapis.com/auth/calendar"]
|
|
68
|
+
DRIVE_SCOPES = ["https://www.googleapis.com/auth/drive"]
|
|
69
|
+
PEOPLE_SCOPES = ["https://www.googleapis.com/auth/contacts"]
|
|
70
|
+
YT_SCOPES = ["https://www.googleapis.com/auth/youtube.readonly"]
|
|
71
|
+
KEEP_SCOPES = [
|
|
72
|
+
"https://www.googleapis.com/auth/keep",
|
|
73
|
+
"https://www.googleapis.com/auth/keep.readonly",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
ALL_SCOPES = sorted(
|
|
77
|
+
set(GMAIL_SCOPES + CAL_SCOPES + DRIVE_SCOPES + PEOPLE_SCOPES + YT_SCOPES)
|
|
78
|
+
)
|
|
79
|
+
ALL_SCOPES_WITH_KEEP = sorted(
|
|
80
|
+
set(GMAIL_SCOPES + CAL_SCOPES + DRIVE_SCOPES + PEOPLE_SCOPES + YT_SCOPES + KEEP_SCOPES)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def __init__(self, *args, **kwargs):
|
|
84
|
+
super(Worker, self).__init__()
|
|
85
|
+
self.signals = WorkerSignals()
|
|
86
|
+
self.args = args
|
|
87
|
+
self.kwargs = kwargs
|
|
88
|
+
self.plugin = None
|
|
89
|
+
self.cmds = None
|
|
90
|
+
self.ctx = None
|
|
91
|
+
self.msg = None
|
|
92
|
+
|
|
93
|
+
# -------------- Core runner --------------
|
|
94
|
+
|
|
95
|
+
@Slot()
|
|
96
|
+
def run(self):
|
|
97
|
+
try:
|
|
98
|
+
responses = []
|
|
99
|
+
for item in self.cmds:
|
|
100
|
+
if self.is_stopped():
|
|
101
|
+
break
|
|
102
|
+
try:
|
|
103
|
+
response = None
|
|
104
|
+
if item["cmd"] in self.plugin.allowed_cmds and self.plugin.has_cmd(item["cmd"]):
|
|
105
|
+
|
|
106
|
+
# Gmail
|
|
107
|
+
if item["cmd"] == "gmail_list_recent":
|
|
108
|
+
response = self.cmd_gmail_list_recent(item)
|
|
109
|
+
elif item["cmd"] == "gmail_list_all":
|
|
110
|
+
response = self.cmd_gmail_list_all(item)
|
|
111
|
+
elif item["cmd"] == "gmail_search":
|
|
112
|
+
response = self.cmd_gmail_search(item)
|
|
113
|
+
elif item["cmd"] == "gmail_get_by_id":
|
|
114
|
+
response = self.cmd_gmail_get_by_id(item)
|
|
115
|
+
elif item["cmd"] == "gmail_send":
|
|
116
|
+
response = self.cmd_gmail_send(item)
|
|
117
|
+
|
|
118
|
+
# Calendar
|
|
119
|
+
elif item["cmd"] == "calendar_events_recent":
|
|
120
|
+
response = self.cmd_calendar_events_recent(item)
|
|
121
|
+
elif item["cmd"] == "calendar_events_today":
|
|
122
|
+
response = self.cmd_calendar_events_today(item)
|
|
123
|
+
elif item["cmd"] == "calendar_events_tomorrow":
|
|
124
|
+
response = self.cmd_calendar_events_tomorrow(item)
|
|
125
|
+
elif item["cmd"] == "calendar_events_all":
|
|
126
|
+
response = self.cmd_calendar_events_all(item)
|
|
127
|
+
elif item["cmd"] == "calendar_events_by_date":
|
|
128
|
+
response = self.cmd_calendar_events_by_date(item)
|
|
129
|
+
elif item["cmd"] == "calendar_add_event":
|
|
130
|
+
response = self.cmd_calendar_add_event(item)
|
|
131
|
+
elif item["cmd"] == "calendar_delete_event":
|
|
132
|
+
response = self.cmd_calendar_delete_event(item)
|
|
133
|
+
|
|
134
|
+
# Keep
|
|
135
|
+
elif item["cmd"] == "keep_list_notes":
|
|
136
|
+
response = self.cmd_keep_list_notes(item)
|
|
137
|
+
elif item["cmd"] == "keep_add_note":
|
|
138
|
+
response = self.cmd_keep_add_note(item)
|
|
139
|
+
|
|
140
|
+
# Drive
|
|
141
|
+
elif item["cmd"] == "drive_list_files":
|
|
142
|
+
response = self.cmd_drive_list_files(item)
|
|
143
|
+
elif item["cmd"] == "drive_find_by_path":
|
|
144
|
+
response = self.cmd_drive_find_by_path(item)
|
|
145
|
+
elif item["cmd"] == "drive_download_file":
|
|
146
|
+
response = self.cmd_drive_download_file(item)
|
|
147
|
+
elif item["cmd"] == "drive_upload_file":
|
|
148
|
+
response = self.cmd_drive_upload_file(item)
|
|
149
|
+
|
|
150
|
+
# YouTube
|
|
151
|
+
elif item["cmd"] == "youtube_video_info":
|
|
152
|
+
response = self.cmd_youtube_video_info(item)
|
|
153
|
+
elif item["cmd"] == "youtube_transcript":
|
|
154
|
+
response = self.cmd_youtube_transcript(item)
|
|
155
|
+
|
|
156
|
+
# Contacts
|
|
157
|
+
elif item["cmd"] == "contacts_list":
|
|
158
|
+
response = self.cmd_contacts_list(item)
|
|
159
|
+
elif item["cmd"] == "contacts_add":
|
|
160
|
+
response = self.cmd_contacts_add(item)
|
|
161
|
+
|
|
162
|
+
if response:
|
|
163
|
+
responses.append(response)
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
responses.append(self.make_response(item, self.throw_error(e)))
|
|
167
|
+
|
|
168
|
+
if responses:
|
|
169
|
+
self.reply_more(responses)
|
|
170
|
+
if self.msg is not None:
|
|
171
|
+
self.status(self.msg)
|
|
172
|
+
except Exception as e:
|
|
173
|
+
self.error(e)
|
|
174
|
+
finally:
|
|
175
|
+
self.cleanup()
|
|
176
|
+
|
|
177
|
+
# -------------- Auth / service helpers --------------
|
|
178
|
+
|
|
179
|
+
def _get_scopes(self) -> List[str]:
|
|
180
|
+
"""
|
|
181
|
+
Determine scopes for the command based on its type.
|
|
182
|
+
|
|
183
|
+
If unofficial Keep is allowed, return all scopes including Keep.
|
|
184
|
+
|
|
185
|
+
:return: List of scopes
|
|
186
|
+
"""
|
|
187
|
+
scopes_str = self.plugin.get_option_value("oauth_scopes") or ""
|
|
188
|
+
if scopes_str:
|
|
189
|
+
scopes = [s.strip() for s in scopes_str.split(" ") if s.strip()]
|
|
190
|
+
if not scopes:
|
|
191
|
+
raise RuntimeError("No valid scopes provided in 'oauth_scopes'")
|
|
192
|
+
return sorted(scopes)
|
|
193
|
+
return []
|
|
194
|
+
|
|
195
|
+
def _get_credentials(self, scopes: List[str]) -> Credentials:
|
|
196
|
+
"""
|
|
197
|
+
Resolve credentials from plugin config. Supports:
|
|
198
|
+
- OAuth client secrets JSON (installed/web) + stored refresh token (oauth_token)
|
|
199
|
+
- Service account JSON (for Workspace scenarios like Keep DWD)
|
|
200
|
+
|
|
201
|
+
:param scopes: List of scopes to request
|
|
202
|
+
:return: Credentials object
|
|
203
|
+
"""
|
|
204
|
+
creds_json_text = self.plugin.get_option_value("credentials") or ""
|
|
205
|
+
if not creds_json_text:
|
|
206
|
+
raise RuntimeError("Missing credentials JSON in plugin option 'credentials'")
|
|
207
|
+
creds_info = json.loads(creds_json_text)
|
|
208
|
+
|
|
209
|
+
# Token store in plugin option to avoid filesystem coupling
|
|
210
|
+
token_text = self.plugin.get_option_value("oauth_token") or None
|
|
211
|
+
creds: Optional[Credentials] = None
|
|
212
|
+
|
|
213
|
+
# If service account
|
|
214
|
+
if isinstance(creds_info, dict) and creds_info.get("type") == "service_account":
|
|
215
|
+
sa_scopes = list(set(scopes))
|
|
216
|
+
creds = ServiceAccountCredentials.from_service_account_info(creds_info, scopes=sa_scopes)
|
|
217
|
+
|
|
218
|
+
# Optional: impersonation for Workspace (domain-wide delegation)
|
|
219
|
+
subject = self.plugin.get_option_value("impersonate_user") or None
|
|
220
|
+
if subject:
|
|
221
|
+
creds = creds.with_subject(subject)
|
|
222
|
+
return creds
|
|
223
|
+
|
|
224
|
+
# OAuth installed/web
|
|
225
|
+
# Try to reuse saved token first
|
|
226
|
+
if token_text:
|
|
227
|
+
try:
|
|
228
|
+
creds = Credentials.from_authorized_user_info(json.loads(token_text), scopes=self._get_scopes())
|
|
229
|
+
except Exception:
|
|
230
|
+
creds = None
|
|
231
|
+
|
|
232
|
+
# Refresh if needed
|
|
233
|
+
if creds and creds.expired and creds.refresh_token:
|
|
234
|
+
creds.refresh(Request())
|
|
235
|
+
|
|
236
|
+
# If no creds or missing refresh, run flow
|
|
237
|
+
if not creds or not creds.valid:
|
|
238
|
+
# Always request the union of scopes to minimize re-prompt later
|
|
239
|
+
flow = InstalledAppFlow.from_client_config(creds_info, scopes=self._get_scopes())
|
|
240
|
+
use_local_server = bool(self.plugin.get_option_value("oauth_local_server") or True)
|
|
241
|
+
if use_local_server:
|
|
242
|
+
creds = flow.run_local_server(port=int(self.plugin.get_option_value("oauth_local_port") or 0))
|
|
243
|
+
else:
|
|
244
|
+
creds = flow.run_console()
|
|
245
|
+
|
|
246
|
+
# Persist token back into plugin options
|
|
247
|
+
self.plugin.set_option_value("oauth_token", creds.to_json())
|
|
248
|
+
|
|
249
|
+
return creds
|
|
250
|
+
|
|
251
|
+
def _service(self, api: str, version: str, scopes: List[str] | None = None, api_key: Optional[str] = None):
|
|
252
|
+
"""
|
|
253
|
+
Build google api service. If api_key is provided, use key-only (no OAuth).
|
|
254
|
+
|
|
255
|
+
:param api: API name (e.g. 'gmail', 'calendar')
|
|
256
|
+
:param version: API version (e.g. 'v1', 'v3')
|
|
257
|
+
:param scopes: List of scopes to request (if OAuth)
|
|
258
|
+
:param api_key: Optional API key for key-only access (no OAuth)
|
|
259
|
+
:return: Google API service object
|
|
260
|
+
"""
|
|
261
|
+
if api_key:
|
|
262
|
+
return build(api, version, developerKey=api_key, cache_discovery=False)
|
|
263
|
+
|
|
264
|
+
creds = self._get_credentials(scopes or self._get_scopes())
|
|
265
|
+
return build(api, version, credentials=creds, cache_discovery=False)
|
|
266
|
+
|
|
267
|
+
# -------------- Gmail --------------
|
|
268
|
+
|
|
269
|
+
def _gmail_message_summary(self, svc, msg_id: str) -> Dict[str, Any]:
|
|
270
|
+
msg = svc.users().messages().get(userId="me", id=msg_id, format="metadata",
|
|
271
|
+
metadataHeaders=["From", "To", "Subject", "Date"]).execute()
|
|
272
|
+
headers = {h["name"]: h["value"] for h in msg.get("payload", {}).get("headers", [])}
|
|
273
|
+
return {
|
|
274
|
+
"id": msg["id"],
|
|
275
|
+
"threadId": msg.get("threadId"),
|
|
276
|
+
"snippet": msg.get("snippet"),
|
|
277
|
+
"internalDate": msg.get("internalDate"),
|
|
278
|
+
"headers": headers,
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
def cmd_gmail_list_recent(self, item: dict) -> dict:
|
|
282
|
+
params = item.get("params", {})
|
|
283
|
+
n = int(params.get("n", 10))
|
|
284
|
+
label_ids = params.get("labelIds") or ["INBOX"]
|
|
285
|
+
q = params.get("q") # optional search string
|
|
286
|
+
|
|
287
|
+
svc = self._service("gmail", "v1", scopes=self.GMAIL_SCOPES)
|
|
288
|
+
res = svc.users().messages().list(userId="me", maxResults=n, labelIds=label_ids, q=q).execute()
|
|
289
|
+
ids = [m["id"] for m in res.get("messages", [])]
|
|
290
|
+
out = [self._gmail_message_summary(svc, mid) for mid in ids]
|
|
291
|
+
return self.make_response(item, out)
|
|
292
|
+
|
|
293
|
+
def cmd_gmail_list_all(self, item: dict) -> dict:
|
|
294
|
+
params = item.get("params", {})
|
|
295
|
+
q = params.get("q")
|
|
296
|
+
label_ids = params.get("labelIds") or None
|
|
297
|
+
limit = params.get("limit") # to protect from huge mailboxes
|
|
298
|
+
limit = int(limit) if limit else None
|
|
299
|
+
|
|
300
|
+
svc = self._service("gmail", "v1", scopes=self.GMAIL_SCOPES)
|
|
301
|
+
all_ids: List[str] = []
|
|
302
|
+
page_token = None
|
|
303
|
+
while True:
|
|
304
|
+
res = svc.users().messages().list(userId="me", pageToken=page_token, q=q, labelIds=label_ids,
|
|
305
|
+
maxResults=500).execute()
|
|
306
|
+
batch = [m["id"] for m in res.get("messages", [])]
|
|
307
|
+
all_ids.extend(batch)
|
|
308
|
+
if limit and len(all_ids) >= limit:
|
|
309
|
+
all_ids = all_ids[:limit]
|
|
310
|
+
break
|
|
311
|
+
page_token = res.get("nextPageToken")
|
|
312
|
+
if not page_token:
|
|
313
|
+
break
|
|
314
|
+
svc_batch = [self._gmail_message_summary(svc, mid) for mid in all_ids]
|
|
315
|
+
return self.make_response(item, svc_batch)
|
|
316
|
+
|
|
317
|
+
def cmd_gmail_search(self, item: dict) -> dict:
|
|
318
|
+
params = item.get("params", {})
|
|
319
|
+
q = params.get("q")
|
|
320
|
+
if not q:
|
|
321
|
+
return self.make_response(item, "Query 'q' required")
|
|
322
|
+
svc = self._service("gmail", "v1", scopes=self.GMAIL_SCOPES)
|
|
323
|
+
res = svc.users().messages().list(userId="me", q=q, maxResults=int(params.get("max", 50))).execute()
|
|
324
|
+
ids = [m["id"] for m in res.get("messages", [])]
|
|
325
|
+
out = [self._gmail_message_summary(svc, mid) for mid in ids]
|
|
326
|
+
return self.make_response(item, out)
|
|
327
|
+
|
|
328
|
+
def _gmail_decode_parts(self, part, collected):
|
|
329
|
+
mime_type = part.get("mimeType", "")
|
|
330
|
+
body = part.get("body", {})
|
|
331
|
+
data = body.get("data")
|
|
332
|
+
if data:
|
|
333
|
+
try:
|
|
334
|
+
text = base64.urlsafe_b64decode(data.encode("utf-8")).decode("utf-8", errors="replace")
|
|
335
|
+
except Exception:
|
|
336
|
+
text = ""
|
|
337
|
+
if mime_type == "text/plain":
|
|
338
|
+
collected["text"] = (collected.get("text", "") + text).strip()
|
|
339
|
+
elif mime_type == "text/html":
|
|
340
|
+
collected["html"] = (collected.get("html", "") + text).strip()
|
|
341
|
+
|
|
342
|
+
for p in part.get("parts", []) or []:
|
|
343
|
+
self._gmail_decode_parts(p, collected)
|
|
344
|
+
|
|
345
|
+
def cmd_gmail_get_by_id(self, item: dict) -> dict:
|
|
346
|
+
params = item.get("params", {})
|
|
347
|
+
mid = params.get("id")
|
|
348
|
+
if not mid:
|
|
349
|
+
return self.make_response(item, "Message 'id' required")
|
|
350
|
+
svc = self._service("gmail", "v1", scopes=self.GMAIL_SCOPES)
|
|
351
|
+
msg = svc.users().messages().get(userId="me", id=mid, format="full").execute()
|
|
352
|
+
headers = {h["name"]: h["value"] for h in msg.get("payload", {}).get("headers", [])}
|
|
353
|
+
content = {"text": "", "html": ""}
|
|
354
|
+
self._gmail_decode_parts(msg.get("payload", {}), content)
|
|
355
|
+
out = {
|
|
356
|
+
"id": msg.get("id"),
|
|
357
|
+
"threadId": msg.get("threadId"),
|
|
358
|
+
"labelIds": msg.get("labelIds"),
|
|
359
|
+
"snippet": msg.get("snippet"),
|
|
360
|
+
"headers": headers,
|
|
361
|
+
"content": content,
|
|
362
|
+
}
|
|
363
|
+
return self.make_response(item, out)
|
|
364
|
+
|
|
365
|
+
def cmd_gmail_send(self, item: dict) -> dict:
|
|
366
|
+
p = item.get("params", {})
|
|
367
|
+
to = p.get("to")
|
|
368
|
+
subject = p.get("subject", "")
|
|
369
|
+
body = p.get("body", "")
|
|
370
|
+
html = bool(p.get("html", False))
|
|
371
|
+
cc = p.get("cc")
|
|
372
|
+
bcc = p.get("bcc")
|
|
373
|
+
attachments = p.get("attachments") or [] # list of local paths
|
|
374
|
+
|
|
375
|
+
if not to:
|
|
376
|
+
return self.make_response(item, "Recipient 'to' required")
|
|
377
|
+
|
|
378
|
+
svc = self._service("gmail", "v1", scopes=self.GMAIL_SCOPES)
|
|
379
|
+
|
|
380
|
+
if attachments:
|
|
381
|
+
msg = MIMEMultipart()
|
|
382
|
+
if html:
|
|
383
|
+
msg.attach(MIMEText(body, "html", "utf-8"))
|
|
384
|
+
else:
|
|
385
|
+
msg.attach(MIMEText(body, "plain", "utf-8"))
|
|
386
|
+
for apath in attachments:
|
|
387
|
+
apath = self.prepare_path(apath)
|
|
388
|
+
if not os.path.exists(apath):
|
|
389
|
+
continue
|
|
390
|
+
with open(apath, "rb") as f:
|
|
391
|
+
part = MIMEBase("application", "octet-stream")
|
|
392
|
+
part.set_payload(f.read())
|
|
393
|
+
encoders.encode_base64(part)
|
|
394
|
+
part.add_header("Content-Disposition", f'attachment; filename="{os.path.basename(apath)}"')
|
|
395
|
+
msg.attach(part)
|
|
396
|
+
else:
|
|
397
|
+
msg = EmailMessage()
|
|
398
|
+
subtype = "html" if html else "plain"
|
|
399
|
+
msg.set_content(body, subtype=subtype, charset="utf-8")
|
|
400
|
+
|
|
401
|
+
msg["To"] = to
|
|
402
|
+
if cc:
|
|
403
|
+
msg["Cc"] = cc
|
|
404
|
+
if bcc:
|
|
405
|
+
msg["Bcc"] = bcc
|
|
406
|
+
msg["Subject"] = subject
|
|
407
|
+
|
|
408
|
+
raw = base64.urlsafe_b64encode(msg.as_bytes()).decode("utf-8")
|
|
409
|
+
sent = svc.users().messages().send(userId="me", body={"raw": raw}).execute()
|
|
410
|
+
return self.make_response(item, {"id": sent.get("id"), "labelIds": sent.get("labelIds")})
|
|
411
|
+
|
|
412
|
+
# -------------- Calendar --------------
|
|
413
|
+
|
|
414
|
+
def _utc_iso(self, dt_obj: dt.datetime) -> str:
|
|
415
|
+
if dt_obj.tzinfo is None:
|
|
416
|
+
dt_obj = dt_obj.replace(tzinfo=dt.timezone.utc)
|
|
417
|
+
return dt_obj.astimezone(dt.timezone.utc).isoformat()
|
|
418
|
+
|
|
419
|
+
def _day_bounds_utc(self, days_from_today: int = 0) -> Tuple[str, str]:
|
|
420
|
+
now = dt.datetime.utcnow()
|
|
421
|
+
day = now.date() + dt.timedelta(days=days_from_today)
|
|
422
|
+
start = dt.datetime(day.year, day.month, day.day, 0, 0, 0, tzinfo=dt.timezone.utc)
|
|
423
|
+
end = start + dt.timedelta(days=1)
|
|
424
|
+
return start.isoformat(), end.isoformat()
|
|
425
|
+
|
|
426
|
+
def _calendar_list(self, svc, time_min: Optional[str], time_max: Optional[str], limit: int) -> List[Dict[str, Any]]:
|
|
427
|
+
res = svc.events().list(
|
|
428
|
+
calendarId="primary",
|
|
429
|
+
timeMin=time_min,
|
|
430
|
+
timeMax=time_max,
|
|
431
|
+
singleEvents=True,
|
|
432
|
+
orderBy="startTime",
|
|
433
|
+
maxResults=limit,
|
|
434
|
+
).execute()
|
|
435
|
+
return res.get("items", [])
|
|
436
|
+
|
|
437
|
+
def cmd_calendar_events_recent(self, item: dict) -> dict:
|
|
438
|
+
p = item.get("params", {})
|
|
439
|
+
limit = int(p.get("limit", 10))
|
|
440
|
+
now_iso = dt.datetime.utcnow().isoformat() + "Z"
|
|
441
|
+
svc = self._service("calendar", "v3", scopes=self.CAL_SCOPES)
|
|
442
|
+
items = self._calendar_list(svc, now_iso, None, limit)
|
|
443
|
+
return self.make_response(item, items)
|
|
444
|
+
|
|
445
|
+
def cmd_calendar_events_today(self, item: dict) -> dict:
|
|
446
|
+
start, end = self._day_bounds_utc(0)
|
|
447
|
+
svc = self._service("calendar", "v3", scopes=self.CAL_SCOPES)
|
|
448
|
+
items = self._calendar_list(svc, start, end, 250)
|
|
449
|
+
return self.make_response(item, items)
|
|
450
|
+
|
|
451
|
+
def cmd_calendar_events_tomorrow(self, item: dict) -> dict:
|
|
452
|
+
start, end = self._day_bounds_utc(1)
|
|
453
|
+
svc = self._service("calendar", "v3", scopes=self.CAL_SCOPES)
|
|
454
|
+
items = self._calendar_list(svc, start, end, 250)
|
|
455
|
+
return self.make_response(item, items)
|
|
456
|
+
|
|
457
|
+
def cmd_calendar_events_all(self, item: dict) -> dict:
|
|
458
|
+
p = item.get("params", {})
|
|
459
|
+
# Sensible default: 1y back and 1y forward
|
|
460
|
+
now = dt.datetime.utcnow()
|
|
461
|
+
time_min = p.get("timeMin") or (now - dt.timedelta(days=365)).isoformat() + "Z"
|
|
462
|
+
time_max = p.get("timeMax") or (now + dt.timedelta(days=365)).isoformat() + "Z"
|
|
463
|
+
svc = self._service("calendar", "v3", scopes=self.CAL_SCOPES)
|
|
464
|
+
items = self._calendar_list(svc, time_min, time_max, 2500)
|
|
465
|
+
return self.make_response(item, items)
|
|
466
|
+
|
|
467
|
+
def cmd_calendar_events_by_date(self, item: dict) -> dict:
|
|
468
|
+
p = item.get("params", {})
|
|
469
|
+
date_str = p.get("date") # YYYY-MM-DD or ISO range start|end
|
|
470
|
+
if not date_str:
|
|
471
|
+
return self.make_response(item, "Param 'date' (YYYY-MM-DD) or 'start'/'end' required")
|
|
472
|
+
if "|" in date_str:
|
|
473
|
+
start, end = date_str.split("|", 1)
|
|
474
|
+
else:
|
|
475
|
+
y, m, d = map(int, date_str.split("-"))
|
|
476
|
+
start_dt = dt.datetime(y, m, d, 0, 0, tzinfo=dt.timezone.utc)
|
|
477
|
+
end_dt = start_dt + dt.timedelta(days=1)
|
|
478
|
+
start, end = start_dt.isoformat(), end_dt.isoformat()
|
|
479
|
+
|
|
480
|
+
svc = self._service("calendar", "v3", scopes=self.CAL_SCOPES)
|
|
481
|
+
items = self._calendar_list(svc, start, end, 2500)
|
|
482
|
+
return self.make_response(item, items)
|
|
483
|
+
|
|
484
|
+
def cmd_calendar_add_event(self, item: dict) -> dict:
|
|
485
|
+
p = item.get("params", {})
|
|
486
|
+
summary = p.get("summary")
|
|
487
|
+
start = p.get("start") # RFC3339 or YYYY-MM-DDTHH:MM
|
|
488
|
+
end = p.get("end")
|
|
489
|
+
tz = p.get("timezone") or "UTC"
|
|
490
|
+
description = p.get("description")
|
|
491
|
+
location = p.get("location")
|
|
492
|
+
attendees = p.get("attendees") or [] # list of emails
|
|
493
|
+
|
|
494
|
+
if not (summary and start and end):
|
|
495
|
+
return self.make_response(item, "Params 'summary', 'start', 'end' are required")
|
|
496
|
+
|
|
497
|
+
def normalize(x: str) -> str:
|
|
498
|
+
if "T" in x:
|
|
499
|
+
if x.endswith("Z") or "+" in x:
|
|
500
|
+
return x
|
|
501
|
+
return f"{x}:00"
|
|
502
|
+
return f"{x}T00:00:00"
|
|
503
|
+
|
|
504
|
+
body = {
|
|
505
|
+
"summary": summary,
|
|
506
|
+
"description": description,
|
|
507
|
+
"location": location,
|
|
508
|
+
"start": {"dateTime": normalize(start), "timeZone": tz},
|
|
509
|
+
"end": {"dateTime": normalize(end), "timeZone": tz},
|
|
510
|
+
}
|
|
511
|
+
if attendees:
|
|
512
|
+
body["attendees"] = [{"email": a} for a in attendees]
|
|
513
|
+
|
|
514
|
+
svc = self._service("calendar", "v3", scopes=self.CAL_SCOPES)
|
|
515
|
+
created = svc.events().insert(calendarId="primary", body=body).execute()
|
|
516
|
+
return self.make_response(item, created)
|
|
517
|
+
|
|
518
|
+
def cmd_calendar_delete_event(self, item: dict) -> dict:
|
|
519
|
+
p = item.get("params", {})
|
|
520
|
+
eid = p.get("event_id")
|
|
521
|
+
if not eid:
|
|
522
|
+
return self.make_response(item, "Param 'event_id' required")
|
|
523
|
+
svc = self._service("calendar", "v3", scopes=self.CAL_SCOPES)
|
|
524
|
+
svc.events().delete(calendarId="primary", eventId=eid).execute()
|
|
525
|
+
return self.make_response(item, "OK")
|
|
526
|
+
|
|
527
|
+
# -------------- Keep --------------
|
|
528
|
+
|
|
529
|
+
def _keep_service(self):
|
|
530
|
+
# Official Keep API (Workspace-focused)
|
|
531
|
+
return self._service("keep", "v1", scopes=self.KEEP_SCOPES)
|
|
532
|
+
|
|
533
|
+
def cmd_keep_list_notes(self, item: dict) -> dict:
|
|
534
|
+
mode = (self.plugin.get_option_value("keep_mode") or "auto").lower()
|
|
535
|
+
if mode in ("official", "auto"):
|
|
536
|
+
try:
|
|
537
|
+
svc = self._keep_service()
|
|
538
|
+
# Official list
|
|
539
|
+
res = svc.notes().list(pageSize=int(item.get("params", {}).get("limit", 50))).execute()
|
|
540
|
+
return self.make_response(item, res.get("notes", []))
|
|
541
|
+
except HttpError as e:
|
|
542
|
+
if mode == "official":
|
|
543
|
+
return self.make_response(item, self.throw_error(e))
|
|
544
|
+
# fallthrough to unofficial if allowed
|
|
545
|
+
|
|
546
|
+
if (self.plugin.get_option_value("allow_unofficial_keep") or False) and gkeepapi:
|
|
547
|
+
# Unofficial fallback (not endorsed by Google)
|
|
548
|
+
email = self.plugin.get_option_value("keep_username")
|
|
549
|
+
token = self.plugin.get_option_value("keep_master_token")
|
|
550
|
+
if not (email and token):
|
|
551
|
+
return self.make_response(item, "Missing keep_username/keep_master_token for unofficial mode")
|
|
552
|
+
keep = gkeepapi.Keep()
|
|
553
|
+
keep.authenticate(email, token)
|
|
554
|
+
keep.sync()
|
|
555
|
+
notes = [{"id": n.id, "title": n.title, "text": getattr(n, "text", "")} for n in keep.all()]
|
|
556
|
+
return self.make_response(item, notes)
|
|
557
|
+
return self.make_response(item, "Keep unavailable (official failed and unofficial disabled)")
|
|
558
|
+
|
|
559
|
+
def cmd_keep_add_note(self, item: dict) -> dict:
|
|
560
|
+
p = item.get("params", {})
|
|
561
|
+
title = p.get("title", "")
|
|
562
|
+
text = p.get("text", "")
|
|
563
|
+
mode = (self.plugin.get_option_value("keep_mode") or "auto").lower()
|
|
564
|
+
|
|
565
|
+
if mode in ("official", "auto"):
|
|
566
|
+
try:
|
|
567
|
+
svc = self._keep_service()
|
|
568
|
+
body = {"title": title, "body": {"text": {"text": text}}}
|
|
569
|
+
created = svc.notes().create(body=body).execute()
|
|
570
|
+
return self.make_response(item, created)
|
|
571
|
+
except HttpError as e:
|
|
572
|
+
if mode == "official":
|
|
573
|
+
return self.make_response(item, self.throw_error(e))
|
|
574
|
+
|
|
575
|
+
if (self.plugin.get_option_value("allow_unofficial_keep") or False) and gkeepapi:
|
|
576
|
+
email = self.plugin.get_option_value("keep_username")
|
|
577
|
+
token = self.plugin.get_option_value("keep_master_token")
|
|
578
|
+
if not (email and token):
|
|
579
|
+
return self.make_response(item, "Missing keep_username/keep_master_token for unofficial mode")
|
|
580
|
+
keep = gkeepapi.Keep()
|
|
581
|
+
keep.authenticate(email, token)
|
|
582
|
+
note = keep.createNote(title, text)
|
|
583
|
+
keep.sync()
|
|
584
|
+
return self.make_response(item, {"id": note.id, "title": title, "text": text})
|
|
585
|
+
|
|
586
|
+
return self.make_response(item, "Keep unavailable (official failed and unofficial disabled)")
|
|
587
|
+
|
|
588
|
+
# -------------- Drive --------------
|
|
589
|
+
|
|
590
|
+
def cmd_drive_list_files(self, item: dict) -> dict:
|
|
591
|
+
p = item.get("params", {})
|
|
592
|
+
q = p.get("q", "trashed=false")
|
|
593
|
+
fields = p.get("fields", "nextPageToken, files(id, name, mimeType, parents)")
|
|
594
|
+
page_size = int(p.get("page_size", 100))
|
|
595
|
+
svc = self._service("drive", "v3", scopes=self.DRIVE_SCOPES)
|
|
596
|
+
res = svc.files().list(q=q, pageSize=page_size, fields=fields).execute()
|
|
597
|
+
return self.make_response(item, res.get("files", []))
|
|
598
|
+
|
|
599
|
+
def _drive_find_by_path(self, svc, path: str) -> Optional[Dict[str, Any]]:
|
|
600
|
+
# Simple path resolver: /Folder/Sub/file.ext in My Drive
|
|
601
|
+
parts = [p for p in Path(path).parts if p not in ("/", "\\")]
|
|
602
|
+
if not parts:
|
|
603
|
+
return None
|
|
604
|
+
parent = "root"
|
|
605
|
+
node = None
|
|
606
|
+
for i, name in enumerate(parts):
|
|
607
|
+
is_last = i == len(parts) - 1
|
|
608
|
+
mime_filter = "" if is_last else " and mimeType = 'application/vnd.google-apps.folder'"
|
|
609
|
+
name_replaced = name.replace("'", "\\'") # Escape single quotes for query
|
|
610
|
+
q = f"name = '{name_replaced}' and '{parent}' in parents and trashed = false{mime_filter}"
|
|
611
|
+
res = svc.files().list(q=q, fields="files(id, name, mimeType, parents)").execute()
|
|
612
|
+
files = res.get("files", [])
|
|
613
|
+
if not files:
|
|
614
|
+
return None
|
|
615
|
+
node = files[0]
|
|
616
|
+
parent = node["id"]
|
|
617
|
+
return node
|
|
618
|
+
|
|
619
|
+
def cmd_drive_find_by_path(self, item: dict) -> dict:
|
|
620
|
+
p = item.get("params", {})
|
|
621
|
+
path = p.get("path")
|
|
622
|
+
if not path:
|
|
623
|
+
return self.make_response(item, "Param 'path' required")
|
|
624
|
+
svc = self._service("drive", "v3", scopes=self.DRIVE_SCOPES)
|
|
625
|
+
node = self._drive_find_by_path(svc, path)
|
|
626
|
+
return self.make_response(item, node or {})
|
|
627
|
+
|
|
628
|
+
def cmd_drive_download_file(self, item: dict) -> dict:
|
|
629
|
+
p = item.get("params", {})
|
|
630
|
+
file_id = p.get("file_id")
|
|
631
|
+
path = p.get("path")
|
|
632
|
+
out_path = self.prepare_path(p.get("out") or "")
|
|
633
|
+
export_mime = p.get("export_mime") # for Google Docs types
|
|
634
|
+
|
|
635
|
+
svc = self._service("drive", "v3", scopes=self.DRIVE_SCOPES)
|
|
636
|
+
|
|
637
|
+
if not file_id and path:
|
|
638
|
+
node = self._drive_find_by_path(svc, path)
|
|
639
|
+
if not node:
|
|
640
|
+
return self.make_response(item, "File not found")
|
|
641
|
+
file_id = node["id"]
|
|
642
|
+
|
|
643
|
+
if not file_id:
|
|
644
|
+
return self.make_response(item, "Param 'file_id' or 'path' required")
|
|
645
|
+
|
|
646
|
+
meta = svc.files().get(fileId=file_id, fields="id, name, mimeType").execute()
|
|
647
|
+
target = out_path or self.prepare_path(meta["name"])
|
|
648
|
+
|
|
649
|
+
fh = io.FileIO(target, "wb")
|
|
650
|
+
try:
|
|
651
|
+
if meta["mimeType"].startswith("application/vnd.google-apps.") and export_mime:
|
|
652
|
+
req = svc.files().export_media(fileId=file_id, mimeType=export_mime)
|
|
653
|
+
else:
|
|
654
|
+
req = svc.files().get_media(fileId=file_id)
|
|
655
|
+
downloader = MediaIoBaseDownload(fh, req)
|
|
656
|
+
done = False
|
|
657
|
+
while not done:
|
|
658
|
+
status, done = downloader.next_chunk()
|
|
659
|
+
return self.make_response(item, {"path": target, "id": meta["id"], "name": meta["name"]})
|
|
660
|
+
finally:
|
|
661
|
+
fh.close()
|
|
662
|
+
|
|
663
|
+
def cmd_drive_upload_file(self, item: dict) -> dict:
|
|
664
|
+
p = item.get("params", {})
|
|
665
|
+
local = self.prepare_path(p.get("local"))
|
|
666
|
+
remote_parent_path = p.get("remote_parent_path") # optional folder path
|
|
667
|
+
name = p.get("name") or os.path.basename(local)
|
|
668
|
+
mime = p.get("mime")
|
|
669
|
+
|
|
670
|
+
if not os.path.exists(local):
|
|
671
|
+
return self.make_response(item, f"Local file not found: {local}")
|
|
672
|
+
|
|
673
|
+
svc = self._service("drive", "v3", scopes=self.DRIVE_SCOPES)
|
|
674
|
+
|
|
675
|
+
parents = None
|
|
676
|
+
if remote_parent_path:
|
|
677
|
+
node = self._drive_find_by_path(svc, remote_parent_path)
|
|
678
|
+
if not node:
|
|
679
|
+
return self.make_response(item, f"Remote parent path not found: {remote_parent_path}")
|
|
680
|
+
parents = [node["id"]]
|
|
681
|
+
|
|
682
|
+
media = MediaFileUpload(local, mimetype=mime, resumable=True)
|
|
683
|
+
body = {"name": name}
|
|
684
|
+
if parents:
|
|
685
|
+
body["parents"] = parents
|
|
686
|
+
|
|
687
|
+
file = svc.files().create(body=body, media_body=media, fields="id, name, mimeType, parents").execute()
|
|
688
|
+
return self.make_response(item, file)
|
|
689
|
+
|
|
690
|
+
# -------------- YouTube --------------
|
|
691
|
+
|
|
692
|
+
def _extract_video_id(self, text: str) -> str:
|
|
693
|
+
# Simple patterns for IDs/URLs
|
|
694
|
+
m = re.search(r"(?:v=|/shorts/|/v/|youtu\.be/)([A-Za-z0-9_-]{11})", text)
|
|
695
|
+
if m:
|
|
696
|
+
return m.group(1)
|
|
697
|
+
if re.match(r"^[A-Za-z0-9_-]{11}$", text):
|
|
698
|
+
return text
|
|
699
|
+
return ""
|
|
700
|
+
|
|
701
|
+
def cmd_youtube_video_info(self, item: dict) -> dict:
|
|
702
|
+
p = item.get("params", {})
|
|
703
|
+
vid = self._extract_video_id(p.get("video", ""))
|
|
704
|
+
if not vid:
|
|
705
|
+
return self.make_response(item, "Param 'video' must be video ID or URL")
|
|
706
|
+
|
|
707
|
+
api_key = self.plugin.get_option_value("youtube_api_key") or None
|
|
708
|
+
svc = self._service("youtube", "v3", scopes=self.YT_SCOPES if not api_key else None, api_key=api_key)
|
|
709
|
+
res = svc.videos().list(id=vid, part="snippet,contentDetails,statistics,status").execute()
|
|
710
|
+
items = res.get("items", [])
|
|
711
|
+
return self.make_response(item, items[0] if items else {})
|
|
712
|
+
|
|
713
|
+
def cmd_youtube_transcript(self, item: dict) -> dict:
|
|
714
|
+
p = item.get("params", {})
|
|
715
|
+
vid = self._extract_video_id(p.get("video", ""))
|
|
716
|
+
lang_pref = p.get("languages") or ["en"]
|
|
717
|
+
official_only = bool(p.get("official_only", False))
|
|
718
|
+
prefer_format = p.get("format") or "srt"
|
|
719
|
+
|
|
720
|
+
# Try official API first (requires ownership/permission)
|
|
721
|
+
try:
|
|
722
|
+
svc = self._service("youtube", "v3", scopes=["https://www.googleapis.com/auth/youtube.force-ssl"])
|
|
723
|
+
cap_list = svc.captions().list(part="id,snippet", videoId=vid).execute().get("items", [])
|
|
724
|
+
if cap_list:
|
|
725
|
+
cap_id = cap_list[0]["id"]
|
|
726
|
+
# download returns binary; google client supports download_media
|
|
727
|
+
req = svc.captions().download_media(id=cap_id, tfmt=prefer_format)
|
|
728
|
+
# Execute media download into memory
|
|
729
|
+
buf = io.BytesIO()
|
|
730
|
+
downloader = MediaIoBaseDownload(buf, req)
|
|
731
|
+
done = False
|
|
732
|
+
while not done:
|
|
733
|
+
status, done = downloader.next_chunk()
|
|
734
|
+
text = buf.getvalue().decode("utf-8", errors="ignore")
|
|
735
|
+
return self.make_response(item, {"videoId": vid, "format": prefer_format, "text": text})
|
|
736
|
+
if official_only:
|
|
737
|
+
return self.make_response(item, "No official captions available or insufficient permissions")
|
|
738
|
+
except HttpError as e:
|
|
739
|
+
if official_only:
|
|
740
|
+
return self.make_response(item, self.throw_error(e))
|
|
741
|
+
|
|
742
|
+
# Fallback (unofficial) – explicit opt-in
|
|
743
|
+
if not (self.plugin.get_option_value("allow_unofficial_youtube_transcript") or False):
|
|
744
|
+
return self.make_response(item, "Unofficial transcript disabled (set allow_unofficial_youtube_transcript)")
|
|
745
|
+
|
|
746
|
+
if YouTubeTranscriptApi is None:
|
|
747
|
+
return self.make_response(item, "youtube-transcript-api not installed")
|
|
748
|
+
|
|
749
|
+
transcripts = YouTubeTranscriptApi.list_transcripts(vid)
|
|
750
|
+
# try preferred languages sequence
|
|
751
|
+
for l in lang_pref:
|
|
752
|
+
try:
|
|
753
|
+
t = transcripts.find_transcript([l]).fetch()
|
|
754
|
+
text = "\n".join([seg["text"] for seg in t])
|
|
755
|
+
return self.make_response(item, {"videoId": vid, "language": l, "text": text})
|
|
756
|
+
except Exception:
|
|
757
|
+
continue
|
|
758
|
+
# try generated
|
|
759
|
+
try:
|
|
760
|
+
t = transcripts.find_generated_transcript(lang_pref).fetch()
|
|
761
|
+
text = "\n".join([seg["text"] for seg in t])
|
|
762
|
+
return self.make_response(item, {"videoId": vid, "language": t.language_code, "text": text})
|
|
763
|
+
except Exception as e:
|
|
764
|
+
return self.make_response(item, self.throw_error(e))
|
|
765
|
+
|
|
766
|
+
# -------------- Contacts (People API) --------------
|
|
767
|
+
|
|
768
|
+
def cmd_contacts_list(self, item: dict) -> dict:
|
|
769
|
+
p = item.get("params", {})
|
|
770
|
+
page_size = int(p.get("page_size", 200))
|
|
771
|
+
person_fields = p.get("person_fields", "names,emailAddresses,phoneNumbers")
|
|
772
|
+
svc = self._service("people", "v1", scopes=self.PEOPLE_SCOPES)
|
|
773
|
+
res = svc.people().connections().list(
|
|
774
|
+
resourceName="people/me",
|
|
775
|
+
pageSize=page_size,
|
|
776
|
+
personFields=person_fields
|
|
777
|
+
).execute()
|
|
778
|
+
return self.make_response(item, res.get("connections", []))
|
|
779
|
+
|
|
780
|
+
def cmd_contacts_add(self, item: dict) -> dict:
|
|
781
|
+
p = item.get("params", {})
|
|
782
|
+
given = p.get("givenName")
|
|
783
|
+
family = p.get("familyName")
|
|
784
|
+
emails = p.get("emails") or []
|
|
785
|
+
phones = p.get("phones") or []
|
|
786
|
+
if not given and not family:
|
|
787
|
+
return self.make_response(item, "Provide at least 'givenName' or 'familyName'")
|
|
788
|
+
|
|
789
|
+
body = {}
|
|
790
|
+
if given or family:
|
|
791
|
+
body["names"] = [{"givenName": given, "familyName": family}]
|
|
792
|
+
if emails:
|
|
793
|
+
body["emailAddresses"] = [{"value": e} for e in emails]
|
|
794
|
+
if phones:
|
|
795
|
+
body["phoneNumbers"] = [{"value": ph} for ph in phones]
|
|
796
|
+
|
|
797
|
+
svc = self._service("people", "v1", scopes=self.PEOPLE_SCOPES)
|
|
798
|
+
created = svc.people().createContact(body=body).execute()
|
|
799
|
+
return self.make_response(item, created)
|
|
800
|
+
|
|
801
|
+
def prepare_path(self, path: str) -> str:
|
|
802
|
+
"""
|
|
803
|
+
Prepare path
|
|
804
|
+
|
|
805
|
+
:param path: path to prepare
|
|
806
|
+
:return: prepared path
|
|
807
|
+
"""
|
|
808
|
+
if path in [".", "./"]:
|
|
809
|
+
return self.plugin.window.core.config.get_user_dir('data')
|
|
810
|
+
|
|
811
|
+
if self.is_absolute_path(path):
|
|
812
|
+
return path
|
|
813
|
+
else:
|
|
814
|
+
return os.path.join(
|
|
815
|
+
self.plugin.window.core.config.get_user_dir('data'),
|
|
816
|
+
path,
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
def is_absolute_path(self, path: str) -> bool:
|
|
820
|
+
"""
|
|
821
|
+
Check if path is absolute
|
|
822
|
+
|
|
823
|
+
:param path: path to check
|
|
824
|
+
:return: True if absolute
|
|
825
|
+
"""
|
|
826
|
+
return os.path.isabs(path)
|