google_calendar_service.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. import logging
  4. from odoo import models, _
  5. from odoo.exceptions import UserError
  6. _logger = logging.getLogger(__name__)
  7. class GoogleCalendarService(models.AbstractModel):
  8. _name = 'google.calendar.service'
  9. _description = 'Google Calendar Service'
  10. def _get_credentials(self, user=None):
  11. """Obtener credenciales de configuración para el usuario especificado"""
  12. if not user:
  13. user = self.env.user
  14. config = self.env['ir.config_parameter'].sudo()
  15. enabled = config.get_param('google_api.enabled', 'False')
  16. if not enabled or enabled == 'False':
  17. raise UserError(_('Google API Integration is not enabled'))
  18. client_id = config.get_param('google_api.client_id', '')
  19. client_secret = config.get_param('google_api.client_secret', '')
  20. if not client_id or not client_secret:
  21. raise UserError(_('Google API credentials not configured'))
  22. # Get access token from user settings
  23. user_settings = user.res_users_settings_id
  24. if not user_settings or not user_settings._google_authenticated():
  25. raise UserError(_('User %s is not authenticated with Google. Please connect your account first.') % user.name)
  26. access_token = user_settings._get_google_access_token()
  27. if not access_token:
  28. raise UserError(_('Could not obtain valid access token for user %s. Please reconnect your Google account.') % user.name)
  29. return {
  30. 'client_id': client_id,
  31. 'client_secret': client_secret,
  32. 'access_token': access_token
  33. }
  34. def _do_request(self, endpoint, method='GET', params=None, json_data=None, headers=None):
  35. """Make a request to Google Calendar API"""
  36. import requests
  37. base_url = 'https://www.googleapis.com/calendar/v3'
  38. url = f"{base_url}{endpoint}"
  39. if headers is None:
  40. headers = {}
  41. try:
  42. # Get credentials (includes access token)
  43. credentials = self._get_credentials()
  44. # Add authorization header
  45. headers['Authorization'] = f'Bearer {credentials["access_token"]}'
  46. if method == 'GET':
  47. response = requests.get(url, params=params, headers=headers)
  48. elif method == 'POST':
  49. response = requests.post(url, params=params, json=json_data, headers=headers)
  50. elif method == 'PUT':
  51. response = requests.put(url, params=params, json=json_data, headers=headers)
  52. elif method == 'DELETE':
  53. response = requests.delete(url, params=params, headers=headers)
  54. else:
  55. raise UserError(_('Unsupported HTTP method: %s') % method)
  56. response.raise_for_status()
  57. if response.status_code == 204: # No content
  58. return None
  59. return response.json()
  60. except requests.exceptions.RequestException as e:
  61. _logger.error(f"Google Calendar API request failed: {str(e)}")
  62. raise UserError(_('Google Calendar API Error: %s') % str(e))
  63. def get_meetings_with_recordings(self, days_back=15):
  64. """Get Google Meet meetings with recordings from the last N days"""
  65. try:
  66. from datetime import datetime, timedelta
  67. # Calculate date range - search for PAST meetings (not future)
  68. end_date = datetime.now()
  69. start_date = end_date - timedelta(days=days_back)
  70. _logger.info(f"Searching for Google Meet events with recordings from {start_date} to {end_date}")
  71. # Get calendar events with conference data (Google Meet events)
  72. # Use timeMin and timeMax to search for PAST events only
  73. time_min = start_date.isoformat() + 'Z'
  74. time_max = end_date.isoformat() + 'Z'
  75. # Get all events with pagination to ensure we get everything
  76. all_events = []
  77. page_token = None
  78. while True:
  79. params = {
  80. 'timeMin': time_min,
  81. 'timeMax': time_max,
  82. 'singleEvents': 'true',
  83. 'orderBy': 'startTime',
  84. 'maxResults': 2500, # Maximum allowed by Google Calendar API
  85. 'fields': 'items(id,summary,start,end,attendees,conferenceData,hangoutLink),nextPageToken'
  86. }
  87. if page_token:
  88. params['pageToken'] = page_token
  89. response = self._do_request('/calendars/primary/events', params=params)
  90. if 'items' in response:
  91. all_events.extend(response['items'])
  92. # Check if there are more pages
  93. page_token = response.get('nextPageToken')
  94. if not page_token:
  95. break
  96. events = {'items': all_events}
  97. _logger.info(f"Retrieved {len(all_events)} total events from Google Calendar API")
  98. # Filter out future events - only process events that have already ended
  99. from datetime import timezone
  100. current_time = datetime.now(timezone.utc)
  101. past_events = []
  102. for event in events.get('items', []):
  103. end_time_str = event['end'].get('dateTime', event['end'].get('date'))
  104. if 'T' in end_time_str:
  105. event_end = datetime.fromisoformat(end_time_str.replace('Z', '+00:00'))
  106. else:
  107. # For date-only events, assume they end at 23:59 UTC
  108. event_end = datetime.fromisoformat(end_time_str + 'T23:59:59+00:00')
  109. # Only include events that have already ended
  110. if event_end < current_time:
  111. past_events.append(event)
  112. else:
  113. _logger.info(f"Skipping future event: {event.get('summary', 'Unknown')} (ends: {event_end})")
  114. _logger.info(f"Found {len(past_events)} past events out of {len(events.get('items', []))} total events")
  115. events = {'items': past_events}
  116. meetings_with_recordings = []
  117. for event in events.get('items', []):
  118. # Check if it's a Google Meet event
  119. if self._is_google_meet_event(event):
  120. event_title = event.get('summary', 'Sin título')
  121. hangout_link = event.get('hangoutLink')
  122. # Get meeting date and time
  123. start_time = event['start'].get('dateTime', event['start'].get('date'))
  124. end_time = event['end'].get('dateTime', event['end'].get('date'))
  125. # Format the date for display
  126. from datetime import datetime
  127. if 'T' in start_time:
  128. meeting_date = datetime.fromisoformat(start_time.replace('Z', '+00:00'))
  129. formatted_date = meeting_date.strftime('%Y-%m-%d %H:%M')
  130. else:
  131. meeting_date = datetime.fromisoformat(start_time)
  132. formatted_date = meeting_date.strftime('%Y-%m-%d')
  133. _logger.info(f"Found Google Meet event: {event_title} ({formatted_date})")
  134. # Try to find ALL files associated with this meeting
  135. meeting_files = self._find_recordings_for_meeting(event, event_title)
  136. if meeting_files:
  137. participants = self._extract_participants(event)
  138. meeting_data = {
  139. 'id': event['id'],
  140. 'title': event_title,
  141. 'start_time': event['start'].get('dateTime', event['start'].get('date')),
  142. 'end_time': event['end'].get('dateTime', event['end'].get('date')),
  143. 'participants': participants,
  144. 'recording_files': meeting_files,
  145. 'hangout_link': hangout_link,
  146. 'calendar_event': event
  147. }
  148. meetings_with_recordings.append(meeting_data)
  149. _logger.info(f"✅ Found meeting with files: {event_title} ({formatted_date}) - Participants: {len(participants)}, Files: {len(meeting_files)})")
  150. else:
  151. _logger.info(f"❌ No files found for meeting: {event_title}")
  152. _logger.info(f"Total meetings with recordings found: {len(meetings_with_recordings)}")
  153. return meetings_with_recordings
  154. except Exception as e:
  155. _logger.error(f"Failed to get meetings with recordings: {str(e)}")
  156. return []
  157. def get_meetings_with_recordings_for_date(self, target_date):
  158. """Get Google Meet meetings with recordings from a specific date
  159. Args:
  160. target_date: datetime.date object representing the target date
  161. """
  162. try:
  163. from datetime import datetime, timedelta
  164. # Convert target_date to datetime range for that specific day
  165. start_date = datetime.combine(target_date, datetime.min.time())
  166. end_date = datetime.combine(target_date, datetime.max.time())
  167. _logger.info(f"Searching for Google Meet events with recordings for specific date: {target_date} ({start_date} to {end_date})")
  168. # Get calendar events with conference data (Google Meet events)
  169. # Use timeMin and timeMax to search for events on the specific date
  170. time_min = start_date.isoformat() + 'Z'
  171. time_max = end_date.isoformat() + 'Z'
  172. # Get all events with pagination to ensure we get everything
  173. all_events = []
  174. page_token = None
  175. while True:
  176. params = {
  177. 'timeMin': time_min,
  178. 'timeMax': time_max,
  179. 'singleEvents': 'true',
  180. 'orderBy': 'startTime',
  181. 'maxResults': 2500, # Maximum allowed by Google Calendar API
  182. 'fields': 'items(id,summary,start,end,attendees,conferenceData,hangoutLink),nextPageToken'
  183. }
  184. if page_token:
  185. params['pageToken'] = page_token
  186. response = self._do_request('/calendars/primary/events', params=params)
  187. if 'items' in response:
  188. all_events.extend(response['items'])
  189. # Check if there are more pages
  190. page_token = response.get('nextPageToken')
  191. if not page_token:
  192. break
  193. events = {'items': all_events}
  194. _logger.info(f"Retrieved {len(all_events)} total events from Google Calendar API for date {target_date}")
  195. # Filter out future events - only process events that have already ended
  196. from datetime import timezone
  197. current_time = datetime.now(timezone.utc)
  198. past_events = []
  199. for event in events.get('items', []):
  200. end_time_str = event['end'].get('dateTime', event['end'].get('date'))
  201. if 'T' in end_time_str:
  202. event_end = datetime.fromisoformat(end_time_str.replace('Z', '+00:00'))
  203. else:
  204. # For date-only events, assume they end at 23:59 UTC
  205. event_end = datetime.fromisoformat(end_time_str + 'T23:59:59+00:00')
  206. # Only include events that have already ended
  207. if event_end < current_time:
  208. past_events.append(event)
  209. else:
  210. _logger.info(f"Skipping future event: {event.get('summary', 'Unknown')} (ends: {event_end})")
  211. _logger.info(f"Found {len(past_events)} past events out of {len(events.get('items', []))} total events for date {target_date}")
  212. events = {'items': past_events}
  213. meetings_with_recordings = []
  214. for event in events.get('items', []):
  215. # Check if it's a Google Meet event
  216. if self._is_google_meet_event(event):
  217. event_title = event.get('summary', 'Sin título')
  218. hangout_link = event.get('hangoutLink')
  219. # Get meeting date and time
  220. start_time = event['start'].get('dateTime', event['start'].get('date'))
  221. end_time = event['end'].get('dateTime', event['end'].get('date'))
  222. # Format the date for display
  223. from datetime import datetime
  224. if 'T' in start_time:
  225. meeting_date = datetime.fromisoformat(start_time.replace('Z', '+00:00'))
  226. formatted_date = meeting_date.strftime('%Y-%m-%d %H:%M')
  227. else:
  228. meeting_date = datetime.fromisoformat(start_time)
  229. formatted_date = meeting_date.strftime('%Y-%m-%d')
  230. _logger.info(f"Found Google Meet event: {event_title} ({formatted_date})")
  231. # Try to find ALL files associated with this meeting
  232. meeting_files = self._find_recordings_for_meeting(event, event_title)
  233. if meeting_files:
  234. participants = self._extract_participants(event)
  235. meeting_data = {
  236. 'id': event['id'],
  237. 'title': event_title,
  238. 'start_time': event['start'].get('dateTime', event['start'].get('date')),
  239. 'end_time': event['end'].get('dateTime', event['end'].get('date')),
  240. 'participants': participants,
  241. 'recording_files': meeting_files,
  242. 'hangout_link': hangout_link,
  243. 'calendar_event': event
  244. }
  245. meetings_with_recordings.append(meeting_data)
  246. _logger.info(f"✅ Found meeting with files: {event_title} ({formatted_date}) - Participants: {len(participants)}, Files: {len(meeting_files)})")
  247. else:
  248. _logger.info(f"❌ No files found for meeting: {event_title}")
  249. _logger.info(f"Total meetings with recordings found for date {target_date}: {len(meetings_with_recordings)}")
  250. return meetings_with_recordings
  251. except Exception as e:
  252. _logger.error(f"Failed to get meetings with recordings for date {target_date}: {str(e)}")
  253. return []
  254. def _find_recordings_for_meeting(self, event, event_title):
  255. """Find ALL files associated with a specific meeting (videos, documents, etc.)"""
  256. try:
  257. from datetime import datetime, timedelta
  258. drive_service = self.env['google.drive.service']
  259. # Search for video files that might be recordings for this meeting
  260. # Use the meeting title and date to find relevant files
  261. start_date = event['start'].get('dateTime', event['start'].get('date'))
  262. # Convert to datetime for search
  263. if 'T' in start_date:
  264. meeting_date = datetime.fromisoformat(start_date.replace('Z', '+00:00'))
  265. else:
  266. meeting_date = datetime.fromisoformat(start_date)
  267. # Search for files created around the meeting time (±1 day)
  268. search_start = (meeting_date - timedelta(days=1)).isoformat()
  269. search_end = (meeting_date + timedelta(days=1)).isoformat()
  270. # Search for ALL file types that might be associated with the meeting
  271. # This includes videos, documents, presentations, transcripts, etc.
  272. files = drive_service._do_request(
  273. '/drive/v3/files',
  274. params={
  275. 'q': f"createdTime > '{search_start}' and createdTime < '{search_end}' and trashed=false",
  276. 'fields': 'files(id,name,createdTime,size,mimeType)',
  277. 'orderBy': 'createdTime desc',
  278. 'pageSize': 50 # Increased to get more files
  279. }
  280. )
  281. meeting_files = []
  282. event_title_clean = self._clean_title_for_matching(event_title)
  283. for file in files.get('files', []):
  284. file_name_clean = self._clean_title_for_matching(file['name'])
  285. mime_type = file.get('mimeType', '')
  286. # Check if file name contains meeting title or vice versa
  287. if (event_title_clean in file_name_clean or
  288. file_name_clean in event_title_clean or
  289. self._titles_match(file['name'], event_title)):
  290. meeting_files.append(file['id'])
  291. # Log with file type indicator
  292. file_type = "📹 Video"
  293. if 'video/' in mime_type:
  294. file_type = "📹 Video"
  295. elif 'application/pdf' in mime_type:
  296. file_type = "📄 PDF"
  297. elif 'application/vnd.google-apps' in mime_type:
  298. file_type = "📊 Google Doc"
  299. elif 'text/' in mime_type:
  300. file_type = "📝 Text"
  301. elif 'image/' in mime_type:
  302. file_type = "🖼️ Image"
  303. else:
  304. file_type = "📁 File"
  305. _logger.info(f"{file_type} Found for meeting '{event_title}': {file['name']} ({mime_type})")
  306. _logger.info(f"📊 Total files found for meeting '{event_title}': {len(meeting_files)}")
  307. return meeting_files
  308. except Exception as e:
  309. _logger.error(f"Error finding recordings for meeting {event_title}: {str(e)}")
  310. return []
  311. def _clean_title_for_matching(self, title):
  312. """Clean title for better matching"""
  313. import re
  314. # Remove common suffixes and prefixes
  315. title = title.lower()
  316. title = re.sub(r' - recording', '', title)
  317. title = re.sub(r' - grabacion', '', title)
  318. title = re.sub(r'recording', '', title)
  319. title = re.sub(r'grabacion', '', title)
  320. # Remove date/time patterns
  321. title = re.sub(r'\d{4}/\d{2}/\d{2}', '', title)
  322. title = re.sub(r'\d{2}:\d{2}', '', title)
  323. title = re.sub(r'cst', '', title)
  324. # Clean up extra spaces
  325. title = ' '.join(title.split())
  326. return title
  327. def _titles_match(self, file_name, event_title):
  328. """Check if file name matches calendar event title"""
  329. # Remove common suffixes from file name
  330. file_clean = file_name.lower()
  331. file_clean = file_clean.replace(' - recording', '').replace(' - grabacion', '')
  332. file_clean = file_clean.replace('recording', '').replace('grabacion', '')
  333. # Remove date/time patterns
  334. import re
  335. file_clean = re.sub(r'\d{4}/\d{2}/\d{2}', '', file_clean)
  336. file_clean = re.sub(r'\d{2}:\d{2}', '', file_clean)
  337. file_clean = re.sub(r'cst', '', file_clean)
  338. # Clean up extra spaces
  339. file_clean = ' '.join(file_clean.split())
  340. # Check if event title is contained in cleaned file name
  341. return event_title.lower() in file_clean or file_clean in event_title.lower()
  342. def _get_parent_folder_info(self, parent_ids, drive_service):
  343. """Get information about parent folders to understand context"""
  344. if not parent_ids:
  345. return {}
  346. try:
  347. parent_info = drive_service._do_request(
  348. f'/drive/v3/files/{parent_ids[0]}',
  349. params={
  350. 'fields': 'id,name,parents'
  351. }
  352. )
  353. return parent_info
  354. except:
  355. return {}
  356. def _is_google_meet_event(self, event):
  357. """Check if event is a Google Meet event"""
  358. # Check for conference data
  359. conference_data = event.get('conferenceData', {})
  360. if conference_data.get('conferenceId'):
  361. return True
  362. # Check for hangout link
  363. if event.get('hangoutLink'):
  364. return True
  365. # Check if attendees have Google Meet links
  366. attendees = event.get('attendees', [])
  367. for attendee in attendees:
  368. if 'hangout.google.com' in str(attendee):
  369. return True
  370. return False
  371. def _extract_participants(self, event):
  372. """Extract participant emails from event"""
  373. participants = []
  374. attendees = event.get('attendees', [])
  375. for attendee in attendees:
  376. email = attendee.get('email')
  377. if email and not email.endswith('@resource.calendar.google.com'):
  378. participants.append(email)
  379. return participants
  380. def _get_meeting_recordings(self, event):
  381. """Get recording files for a meeting"""
  382. # This is a simplified implementation
  383. # In a real scenario, you would need to:
  384. # 1. Get the meeting ID from the event
  385. # 2. Call Google Drive API to search for recording files
  386. # 3. Filter files that belong to this specific meeting
  387. try:
  388. # For now, we'll search for any recording files in the user's Drive
  389. # that might be related to meetings
  390. drive_service = self.env['google.drive.service']
  391. # Search for video files created in the last few days
  392. from datetime import datetime, timedelta
  393. cutoff_date = (datetime.now() - timedelta(days=2)).isoformat()
  394. files = drive_service._do_request(
  395. '/drive/v3/files',
  396. params={
  397. 'q': f"mimeType contains 'video/' and createdTime > '{cutoff_date}' and trashed=false",
  398. 'fields': 'files(id,name,createdTime,parents)',
  399. 'orderBy': 'createdTime desc',
  400. 'pageSize': 50
  401. }
  402. )
  403. recording_files = []
  404. for file in files.get('files', []):
  405. # Simple heuristic: if file name contains meeting-related keywords
  406. file_name = file['name'].lower()
  407. if any(keyword in file_name for keyword in ['meet', 'meeting', 'reunion', 'grabacion', 'recording']):
  408. recording_files.append(file['id'])
  409. return recording_files
  410. except Exception as e:
  411. _logger.error(f"Failed to get meeting recordings: {str(e)}")
  412. return []
  413. def get_calendar_list(self):
  414. """Get list of available calendars"""
  415. try:
  416. calendars = self._do_request(
  417. '/users/me/calendarList'
  418. )
  419. return calendars.get('items', [])
  420. except Exception as e:
  421. _logger.error(f"Failed to get calendar list: {str(e)}")
  422. raise UserError(_('Failed to get calendar list: %s') % str(e))