# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging from odoo import models, _ from odoo.exceptions import UserError _logger = logging.getLogger(__name__) class GoogleCalendarService(models.AbstractModel): _name = 'google.calendar.service' _description = 'Google Calendar Service' def _get_credentials(self, user=None): """Obtener credenciales de configuración para el usuario especificado""" if not user: user = self.env.user config = self.env['ir.config_parameter'].sudo() enabled = config.get_param('google_api.enabled', 'False') if not enabled or enabled == 'False': raise UserError(_('Google API Integration is not enabled')) client_id = config.get_param('google_api.client_id', '') client_secret = config.get_param('google_api.client_secret', '') if not client_id or not client_secret: raise UserError(_('Google API credentials not configured')) # Get access token from user settings user_settings = user.res_users_settings_id if not user_settings or not user_settings._google_authenticated(): raise UserError(_('User %s is not authenticated with Google. Please connect your account first.') % user.name) access_token = user_settings._get_google_access_token() if not access_token: raise UserError(_('Could not obtain valid access token for user %s. Please reconnect your Google account.') % user.name) return { 'client_id': client_id, 'client_secret': client_secret, 'access_token': access_token } def _do_request(self, endpoint, method='GET', params=None, json_data=None, headers=None): """Make a request to Google Calendar API""" import requests base_url = 'https://www.googleapis.com/calendar/v3' url = f"{base_url}{endpoint}" if headers is None: headers = {} try: # Get credentials (includes access token) credentials = self._get_credentials() # Add authorization header headers['Authorization'] = f'Bearer {credentials["access_token"]}' if method == 'GET': response = requests.get(url, params=params, headers=headers) elif method == 'POST': response = requests.post(url, params=params, json=json_data, headers=headers) elif method == 'PUT': response = requests.put(url, params=params, json=json_data, headers=headers) elif method == 'DELETE': response = requests.delete(url, params=params, headers=headers) else: raise UserError(_('Unsupported HTTP method: %s') % method) response.raise_for_status() if response.status_code == 204: # No content return None return response.json() except requests.exceptions.RequestException as e: _logger.error(f"Google Calendar API request failed: {str(e)}") raise UserError(_('Google Calendar API Error: %s') % str(e)) def get_meetings_with_recordings(self, days_back=15): """Get Google Meet meetings with recordings from the last N days""" try: from datetime import datetime, timedelta # Calculate date range - search for PAST meetings (not future) end_date = datetime.now() start_date = end_date - timedelta(days=days_back) _logger.info(f"Searching for Google Meet events with recordings from {start_date} to {end_date}") # Get calendar events with conference data (Google Meet events) # Use timeMin and timeMax to search for PAST events only time_min = start_date.isoformat() + 'Z' time_max = end_date.isoformat() + 'Z' # Get all events with pagination to ensure we get everything all_events = [] page_token = None while True: params = { 'timeMin': time_min, 'timeMax': time_max, 'singleEvents': 'true', 'orderBy': 'startTime', 'maxResults': 2500, # Maximum allowed by Google Calendar API 'fields': 'items(id,summary,start,end,attendees,conferenceData,hangoutLink),nextPageToken' } if page_token: params['pageToken'] = page_token response = self._do_request('/calendars/primary/events', params=params) if 'items' in response: all_events.extend(response['items']) # Check if there are more pages page_token = response.get('nextPageToken') if not page_token: break events = {'items': all_events} _logger.info(f"Retrieved {len(all_events)} total events from Google Calendar API") # Filter out future events - only process events that have already ended from datetime import timezone current_time = datetime.now(timezone.utc) past_events = [] for event in events.get('items', []): end_time_str = event['end'].get('dateTime', event['end'].get('date')) if 'T' in end_time_str: event_end = datetime.fromisoformat(end_time_str.replace('Z', '+00:00')) else: # For date-only events, assume they end at 23:59 UTC event_end = datetime.fromisoformat(end_time_str + 'T23:59:59+00:00') # Only include events that have already ended if event_end < current_time: past_events.append(event) else: _logger.info(f"Skipping future event: {event.get('summary', 'Unknown')} (ends: {event_end})") _logger.info(f"Found {len(past_events)} past events out of {len(events.get('items', []))} total events") events = {'items': past_events} meetings_with_recordings = [] for event in events.get('items', []): # Check if it's a Google Meet event if self._is_google_meet_event(event): event_title = event.get('summary', 'Sin título') hangout_link = event.get('hangoutLink') # Get meeting date and time start_time = event['start'].get('dateTime', event['start'].get('date')) end_time = event['end'].get('dateTime', event['end'].get('date')) # Format the date for display from datetime import datetime if 'T' in start_time: meeting_date = datetime.fromisoformat(start_time.replace('Z', '+00:00')) formatted_date = meeting_date.strftime('%Y-%m-%d %H:%M') else: meeting_date = datetime.fromisoformat(start_time) formatted_date = meeting_date.strftime('%Y-%m-%d') _logger.info(f"Found Google Meet event: {event_title} ({formatted_date})") # Try to find ALL files associated with this meeting meeting_files = self._find_recordings_for_meeting(event, event_title) if meeting_files: participants = self._extract_participants(event) meeting_data = { 'id': event['id'], 'title': event_title, 'start_time': event['start'].get('dateTime', event['start'].get('date')), 'end_time': event['end'].get('dateTime', event['end'].get('date')), 'participants': participants, 'recording_files': meeting_files, 'hangout_link': hangout_link, 'calendar_event': event } meetings_with_recordings.append(meeting_data) _logger.info(f"✅ Found meeting with files: {event_title} ({formatted_date}) - Participants: {len(participants)}, Files: {len(meeting_files)})") else: _logger.info(f"❌ No files found for meeting: {event_title}") _logger.info(f"Total meetings with recordings found: {len(meetings_with_recordings)}") return meetings_with_recordings except Exception as e: _logger.error(f"Failed to get meetings with recordings: {str(e)}") return [] def get_meetings_with_recordings_for_date(self, target_date): """Get Google Meet meetings with recordings from a specific date Args: target_date: datetime.date object representing the target date """ try: from datetime import datetime, timedelta # Convert target_date to datetime range for that specific day start_date = datetime.combine(target_date, datetime.min.time()) end_date = datetime.combine(target_date, datetime.max.time()) _logger.info(f"Searching for Google Meet events with recordings for specific date: {target_date} ({start_date} to {end_date})") # Get calendar events with conference data (Google Meet events) # Use timeMin and timeMax to search for events on the specific date time_min = start_date.isoformat() + 'Z' time_max = end_date.isoformat() + 'Z' # Get all events with pagination to ensure we get everything all_events = [] page_token = None while True: params = { 'timeMin': time_min, 'timeMax': time_max, 'singleEvents': 'true', 'orderBy': 'startTime', 'maxResults': 2500, # Maximum allowed by Google Calendar API 'fields': 'items(id,summary,start,end,attendees,conferenceData,hangoutLink),nextPageToken' } if page_token: params['pageToken'] = page_token response = self._do_request('/calendars/primary/events', params=params) if 'items' in response: all_events.extend(response['items']) # Check if there are more pages page_token = response.get('nextPageToken') if not page_token: break events = {'items': all_events} _logger.info(f"Retrieved {len(all_events)} total events from Google Calendar API for date {target_date}") # Filter out future events - only process events that have already ended from datetime import timezone current_time = datetime.now(timezone.utc) past_events = [] for event in events.get('items', []): end_time_str = event['end'].get('dateTime', event['end'].get('date')) if 'T' in end_time_str: event_end = datetime.fromisoformat(end_time_str.replace('Z', '+00:00')) else: # For date-only events, assume they end at 23:59 UTC event_end = datetime.fromisoformat(end_time_str + 'T23:59:59+00:00') # Only include events that have already ended if event_end < current_time: past_events.append(event) else: _logger.info(f"Skipping future event: {event.get('summary', 'Unknown')} (ends: {event_end})") _logger.info(f"Found {len(past_events)} past events out of {len(events.get('items', []))} total events for date {target_date}") events = {'items': past_events} meetings_with_recordings = [] for event in events.get('items', []): # Check if it's a Google Meet event if self._is_google_meet_event(event): event_title = event.get('summary', 'Sin título') hangout_link = event.get('hangoutLink') # Get meeting date and time start_time = event['start'].get('dateTime', event['start'].get('date')) end_time = event['end'].get('dateTime', event['end'].get('date')) # Format the date for display from datetime import datetime if 'T' in start_time: meeting_date = datetime.fromisoformat(start_time.replace('Z', '+00:00')) formatted_date = meeting_date.strftime('%Y-%m-%d %H:%M') else: meeting_date = datetime.fromisoformat(start_time) formatted_date = meeting_date.strftime('%Y-%m-%d') _logger.info(f"Found Google Meet event: {event_title} ({formatted_date})") # Try to find ALL files associated with this meeting meeting_files = self._find_recordings_for_meeting(event, event_title) if meeting_files: participants = self._extract_participants(event) meeting_data = { 'id': event['id'], 'title': event_title, 'start_time': event['start'].get('dateTime', event['start'].get('date')), 'end_time': event['end'].get('dateTime', event['end'].get('date')), 'participants': participants, 'recording_files': meeting_files, 'hangout_link': hangout_link, 'calendar_event': event } meetings_with_recordings.append(meeting_data) _logger.info(f"✅ Found meeting with files: {event_title} ({formatted_date}) - Participants: {len(participants)}, Files: {len(meeting_files)})") else: _logger.info(f"❌ No files found for meeting: {event_title}") _logger.info(f"Total meetings with recordings found for date {target_date}: {len(meetings_with_recordings)}") return meetings_with_recordings except Exception as e: _logger.error(f"Failed to get meetings with recordings for date {target_date}: {str(e)}") return [] def _find_recordings_for_meeting(self, event, event_title): """Find ALL files associated with a specific meeting (videos, documents, etc.)""" try: from datetime import datetime, timedelta drive_service = self.env['google.drive.service'] # Search for video files that might be recordings for this meeting # Use the meeting title and date to find relevant files start_date = event['start'].get('dateTime', event['start'].get('date')) # Convert to datetime for search if 'T' in start_date: meeting_date = datetime.fromisoformat(start_date.replace('Z', '+00:00')) else: meeting_date = datetime.fromisoformat(start_date) # Search for files created around the meeting time (±1 day) search_start = (meeting_date - timedelta(days=1)).isoformat() search_end = (meeting_date + timedelta(days=1)).isoformat() # Search for ALL file types that might be associated with the meeting # This includes videos, documents, presentations, transcripts, etc. files = drive_service._do_request( '/drive/v3/files', params={ 'q': f"createdTime > '{search_start}' and createdTime < '{search_end}' and trashed=false", 'fields': 'files(id,name,createdTime,size,mimeType)', 'orderBy': 'createdTime desc', 'pageSize': 50 # Increased to get more files } ) meeting_files = [] event_title_clean = self._clean_title_for_matching(event_title) for file in files.get('files', []): file_name_clean = self._clean_title_for_matching(file['name']) mime_type = file.get('mimeType', '') # Check if file name contains meeting title or vice versa if (event_title_clean in file_name_clean or file_name_clean in event_title_clean or self._titles_match(file['name'], event_title)): meeting_files.append(file['id']) # Log with file type indicator file_type = "📹 Video" if 'video/' in mime_type: file_type = "📹 Video" elif 'application/pdf' in mime_type: file_type = "📄 PDF" elif 'application/vnd.google-apps' in mime_type: file_type = "📊 Google Doc" elif 'text/' in mime_type: file_type = "📝 Text" elif 'image/' in mime_type: file_type = "🖼️ Image" else: file_type = "📁 File" _logger.info(f"{file_type} Found for meeting '{event_title}': {file['name']} ({mime_type})") _logger.info(f"📊 Total files found for meeting '{event_title}': {len(meeting_files)}") return meeting_files except Exception as e: _logger.error(f"Error finding recordings for meeting {event_title}: {str(e)}") return [] def _clean_title_for_matching(self, title): """Clean title for better matching""" import re # Remove common suffixes and prefixes title = title.lower() title = re.sub(r' - recording', '', title) title = re.sub(r' - grabacion', '', title) title = re.sub(r'recording', '', title) title = re.sub(r'grabacion', '', title) # Remove date/time patterns title = re.sub(r'\d{4}/\d{2}/\d{2}', '', title) title = re.sub(r'\d{2}:\d{2}', '', title) title = re.sub(r'cst', '', title) # Clean up extra spaces title = ' '.join(title.split()) return title def _titles_match(self, file_name, event_title): """Check if file name matches calendar event title""" # Remove common suffixes from file name file_clean = file_name.lower() file_clean = file_clean.replace(' - recording', '').replace(' - grabacion', '') file_clean = file_clean.replace('recording', '').replace('grabacion', '') # Remove date/time patterns import re file_clean = re.sub(r'\d{4}/\d{2}/\d{2}', '', file_clean) file_clean = re.sub(r'\d{2}:\d{2}', '', file_clean) file_clean = re.sub(r'cst', '', file_clean) # Clean up extra spaces file_clean = ' '.join(file_clean.split()) # Check if event title is contained in cleaned file name return event_title.lower() in file_clean or file_clean in event_title.lower() def _get_parent_folder_info(self, parent_ids, drive_service): """Get information about parent folders to understand context""" if not parent_ids: return {} try: parent_info = drive_service._do_request( f'/drive/v3/files/{parent_ids[0]}', params={ 'fields': 'id,name,parents' } ) return parent_info except: return {} def _is_google_meet_event(self, event): """Check if event is a Google Meet event""" # Check for conference data conference_data = event.get('conferenceData', {}) if conference_data.get('conferenceId'): return True # Check for hangout link if event.get('hangoutLink'): return True # Check if attendees have Google Meet links attendees = event.get('attendees', []) for attendee in attendees: if 'hangout.google.com' in str(attendee): return True return False def _extract_participants(self, event): """Extract participant emails from event""" participants = [] attendees = event.get('attendees', []) for attendee in attendees: email = attendee.get('email') if email and not email.endswith('@resource.calendar.google.com'): participants.append(email) return participants def _get_meeting_recordings(self, event): """Get recording files for a meeting""" # This is a simplified implementation # In a real scenario, you would need to: # 1. Get the meeting ID from the event # 2. Call Google Drive API to search for recording files # 3. Filter files that belong to this specific meeting try: # For now, we'll search for any recording files in the user's Drive # that might be related to meetings drive_service = self.env['google.drive.service'] # Search for video files created in the last few days from datetime import datetime, timedelta cutoff_date = (datetime.now() - timedelta(days=2)).isoformat() files = drive_service._do_request( '/drive/v3/files', params={ 'q': f"mimeType contains 'video/' and createdTime > '{cutoff_date}' and trashed=false", 'fields': 'files(id,name,createdTime,parents)', 'orderBy': 'createdTime desc', 'pageSize': 50 } ) recording_files = [] for file in files.get('files', []): # Simple heuristic: if file name contains meeting-related keywords file_name = file['name'].lower() if any(keyword in file_name for keyword in ['meet', 'meeting', 'reunion', 'grabacion', 'recording']): recording_files.append(file['id']) return recording_files except Exception as e: _logger.error(f"Failed to get meeting recordings: {str(e)}") return [] def get_calendar_list(self): """Get list of available calendars""" try: calendars = self._do_request( '/users/me/calendarList' ) return calendars.get('items', []) except Exception as e: _logger.error(f"Failed to get calendar list: {str(e)}") raise UserError(_('Failed to get calendar list: %s') % str(e))