|
5 | 5 | import time |
6 | 6 | import os |
7 | 7 | from copy import deepcopy |
8 | | -from datetime import datetime, date, timedelta |
| 8 | +from datetime import datetime, date, timedelta, timezone |
9 | 9 | from queue import Full |
10 | 10 | from urllib.parse import unquote, urlparse, parse_qs |
11 | 11 | from uuid import UUID |
|
40 | 40 | has_user_filter, is_user_filter |
41 | 41 | from dtable_events.utils.universal_app_api import UniversalAppAPI |
42 | 42 | from dtable_events.utils.email_sender import EmailSender |
| 43 | +from dtable_events.utils.google_calendar_manager import CalendarManager |
43 | 44 |
|
44 | 45 | PER_DAY = 'per_day' |
45 | 46 | PER_WEEK = 'per_week' |
@@ -4073,6 +4074,255 @@ def do_action(self): |
4073 | 4074 | return |
4074 | 4075 |
|
4075 | 4076 |
|
| 4077 | +class GoogleCalendar(BaseAction): |
| 4078 | + def __init__(self, auto_rule, action_type, data, config, function_type): |
| 4079 | + super().__init__(auto_rule, action_type, data) |
| 4080 | + self.config = config or {} |
| 4081 | + self.function_type = function_type |
| 4082 | + self.account_id = self.config.get('account_id') |
| 4083 | + self.calendar_id = self.config.get('calendar_id') |
| 4084 | + self.col_key_dict = {col.get('key'): col for col in self.auto_rule.table_info['columns']} |
| 4085 | + |
| 4086 | + def update_event(self): |
| 4087 | + event_id_key = self.config.get('event_id_key') |
| 4088 | + |
| 4089 | + # Get event_id from the specified column |
| 4090 | + event_id = self._get_field_value_by_column_key(event_id_key) |
| 4091 | + if not event_id: |
| 4092 | + auto_rule_logger.error(f'rule {self.auto_rule.rule_id} GoogleCalendar update event failed: event ID not found.') |
| 4093 | + return |
| 4094 | + |
| 4095 | + try: |
| 4096 | + event_data = self._prepare_event_data() |
| 4097 | + if not event_data: |
| 4098 | + auto_rule_logger.error(f'rule {self.auto_rule.rule_id} GoogleCalendar update event failed: unable to prepare event data from table row') |
| 4099 | + return |
| 4100 | + |
| 4101 | + # Add event_id for update operation |
| 4102 | + event_data['event_id'] = event_id |
| 4103 | + |
| 4104 | + manager = CalendarManager(self.account_id, db_session=self.auto_rule.db_session) |
| 4105 | + result = manager.update_event(event_data) |
| 4106 | + |
| 4107 | + if result.get('error_msg'): |
| 4108 | + auto_rule_logger.error(f'rule {self.auto_rule.rule_id} GoogleCalendar update event failed: {result["error_msg"]}') |
| 4109 | + else: |
| 4110 | + auto_rule_logger.info(f'rule {self.auto_rule.rule_id} GoogleCalendar update event success') |
| 4111 | + |
| 4112 | + except Exception as e: |
| 4113 | + auto_rule_logger.error(f'rule {self.auto_rule.rule_id} GoogleCalendar update event failed with error: {e}') |
| 4114 | + |
| 4115 | + def create_event(self): |
| 4116 | + try: |
| 4117 | + event_data = self._prepare_event_data() |
| 4118 | + if not event_data: |
| 4119 | + auto_rule_logger.error(f'rule {self.auto_rule.rule_id} GoogleCalendar create event failed: unable to prepare event data from table row') |
| 4120 | + return |
| 4121 | + |
| 4122 | + manager = CalendarManager(self.account_id, db_session=self.auto_rule.db_session) |
| 4123 | + result = manager.create_event(event_data) |
| 4124 | + |
| 4125 | + if result.get('error_msg'): |
| 4126 | + auto_rule_logger.error(f'rule {self.auto_rule.rule_id} GoogleCalendar create event failed: {result["error_msg"]}') |
| 4127 | + elif result.get('id'): |
| 4128 | + event_id = result.get('id') |
| 4129 | + self._save_event_id_to_column(event_id) |
| 4130 | + |
| 4131 | + except Exception as e: |
| 4132 | + auto_rule_logger.error(f'rule {self.auto_rule.rule_id} GoogleCalendar create event failed with error: {e}') |
| 4133 | + |
| 4134 | + def _prepare_event_data(self): |
| 4135 | + try: |
| 4136 | + start_date_column_key = self.config.get('start_date_column_key') |
| 4137 | + end_date_column_key = self.config.get('end_date_column_key') |
| 4138 | + |
| 4139 | + start_time = self._get_field_value_by_column_key(start_date_column_key) |
| 4140 | + end_time = self._get_field_value_by_column_key(end_date_column_key) |
| 4141 | + |
| 4142 | + event_data = { |
| 4143 | + 'summary': self._get_field_value('event_title'), |
| 4144 | + 'start': self._format_datetime_object(start_time), |
| 4145 | + 'end': self._format_datetime_object(end_time), |
| 4146 | + 'calendar_id': self.calendar_id, |
| 4147 | + 'description': self._get_field_value('event_description'), |
| 4148 | + 'location': self._get_field_value('location'), |
| 4149 | + 'attendees': self._format_attendees(self.config.get('attendees', [])), |
| 4150 | + } |
| 4151 | + |
| 4152 | + if not self.config.get('dont_send_notifications'): |
| 4153 | + event_data['sendUpdates'] = 'all' |
| 4154 | + if self.config.get('guests_can_invite') is not None: |
| 4155 | + event_data['guestsCanInviteOthers'] = self.config.get('guests_can_invite') |
| 4156 | + if self.config.get('guests_can_modify') is not None: |
| 4157 | + event_data['guestsCanModify'] = self.config.get('guests_can_modify') |
| 4158 | + if self.config.get('guests_can_see_list') is not None: |
| 4159 | + event_data['guestsCanSeeOtherGuests'] = self.config.get('guests_can_see_list') |
| 4160 | + |
| 4161 | + if self.config.get('video_conferencing'): |
| 4162 | + event_data['conferenceDataVersion'] = 1 |
| 4163 | + event_data['conferenceData'] = { |
| 4164 | + 'createRequest': { |
| 4165 | + 'requestId': f"dtable_event_{self.auto_rule.rule_id}_{int(time.time())}", |
| 4166 | + 'conferenceSolutionKey': { |
| 4167 | + 'type': 'hangoutsMeet' |
| 4168 | + } |
| 4169 | + } |
| 4170 | + } |
| 4171 | + |
| 4172 | + return event_data |
| 4173 | + |
| 4174 | + except Exception as e: |
| 4175 | + auto_rule_logger.error(f'rule {self.auto_rule.rule_id} GoogleCalendar prepare event data failed: {e}') |
| 4176 | + return None |
| 4177 | + |
| 4178 | + def _get_field_value_by_column_key(self, column_key): |
| 4179 | + if not column_key: |
| 4180 | + return None |
| 4181 | + |
| 4182 | + sql_row = self.auto_rule.get_sql_row() |
| 4183 | + if not sql_row: |
| 4184 | + return None |
| 4185 | + |
| 4186 | + return sql_row.get(column_key) |
| 4187 | + |
| 4188 | + def _get_field_value(self, field_name): |
| 4189 | + field_value = self.config.get(field_name) |
| 4190 | + if field_value and not isinstance(field_value, str): |
| 4191 | + return '' |
| 4192 | + blanks = set(re.findall(r'\{([^{]*?)\}', field_value)) |
| 4193 | + col_name_dict = {col['name']: col for col in self.auto_rule.table_info['columns']} |
| 4194 | + sql_row = self.auto_rule.get_sql_row() |
| 4195 | + return fill_msg_blanks_with_sql_row(field_value, blanks, col_name_dict, sql_row, self.auto_rule.db_session) |
| 4196 | + |
| 4197 | + def _format_datetime_object(self, datetime_str): |
| 4198 | + |
| 4199 | + if not datetime_str: |
| 4200 | + return None |
| 4201 | + |
| 4202 | + try: |
| 4203 | + if isinstance(datetime_str, str): |
| 4204 | + dt = datetime.fromisoformat(datetime_str.replace('Z', '+00:00')) |
| 4205 | + else: |
| 4206 | + dt = datetime_str |
| 4207 | + |
| 4208 | + if dt.tzinfo is None: |
| 4209 | + dt = dt.replace(tzinfo=timezone.utc) |
| 4210 | + |
| 4211 | + return { |
| 4212 | + 'dateTime': dt.isoformat(), |
| 4213 | + 'timeZone': 'UTC' if dt.tzinfo == timezone.utc else str(dt.tzinfo) |
| 4214 | + } |
| 4215 | + |
| 4216 | + except Exception as e: |
| 4217 | + auto_rule_logger.error(f'rule {self.auto_rule.rule_id} GoogleCalendar datetime formatting failed: {e}') |
| 4218 | + return None |
| 4219 | + |
| 4220 | + def _format_attendees(self, attendees): |
| 4221 | + if not attendees: |
| 4222 | + return [] |
| 4223 | + |
| 4224 | + formatted_attendees = [] |
| 4225 | + try: |
| 4226 | + # Convert usernames to contact emails |
| 4227 | + attendee_emails = self._convert_usernames_to_emails(attendees) |
| 4228 | + |
| 4229 | + for attendee in attendee_emails: |
| 4230 | + formatted_attendees.append({ |
| 4231 | + 'email': attendee, |
| 4232 | + 'responseStatus': 'needsAction' |
| 4233 | + }) |
| 4234 | + |
| 4235 | + except Exception as e: |
| 4236 | + auto_rule_logger.error(f'rule {self.auto_rule.rule_id} GoogleCalendar attendees formatting failed: {e}') |
| 4237 | + |
| 4238 | + return formatted_attendees |
| 4239 | + |
| 4240 | + def _convert_usernames_to_emails(self, attendees): |
| 4241 | + """Convert usernames to contact emails.""" |
| 4242 | + return_emails = [] |
| 4243 | + usernames = [] |
| 4244 | + for item in attendees: |
| 4245 | + if not is_valid_email(item): |
| 4246 | + continue |
| 4247 | + if '@auth.local' in item: |
| 4248 | + usernames.append(item) |
| 4249 | + continue |
| 4250 | + if item in return_emails: |
| 4251 | + continue |
| 4252 | + return_emails.append(item) |
| 4253 | + if usernames: |
| 4254 | + sql = ''' |
| 4255 | + SELECT `contact_email` FROM `profile_profile` |
| 4256 | + WHERE `user` IN :usernames AND `contact_email` IS NOT NULL AND `contact_email` != '' |
| 4257 | + ''' |
| 4258 | + try: |
| 4259 | + results = self.auto_rule.db_session.execute(text(sql), {'usernames': usernames}) |
| 4260 | + except Exception as e: |
| 4261 | + auto_rule_logger.error(f'rule {self.auto_rule.rule_id} GoogleCalendar failed to query user contact emails: {e}') |
| 4262 | + else: |
| 4263 | + return_emails.extend([item.contact_email for item in results]) |
| 4264 | + return return_emails |
| 4265 | + |
| 4266 | + def _save_event_id_to_column(self, event_id): |
| 4267 | + """Save event_id to the configured event_id column""" |
| 4268 | + try: |
| 4269 | + table_name = self.auto_rule.table_info['name'] |
| 4270 | + row_id = self.data['row_id'] |
| 4271 | + event_id_key = self.config.get('event_id_key') |
| 4272 | + |
| 4273 | + event_id_column = self.col_key_dict.get(event_id_key) |
| 4274 | + if not event_id_column: |
| 4275 | + auto_rule_logger.error(f'rule {self.auto_rule.rule_id} GoogleCalendar event_id column not found') |
| 4276 | + return |
| 4277 | + |
| 4278 | + column_name = event_id_column.get('name') |
| 4279 | + update_data = {column_name: event_id} |
| 4280 | + self.auto_rule.dtable_server_api.update_row(table_name, row_id, update_data) |
| 4281 | + |
| 4282 | + except Exception as e: |
| 4283 | + auto_rule_logger.error(f'rule {self.auto_rule.rule_id} GoogleCalendar failed to save event_id: {e}') |
| 4284 | + |
| 4285 | + def can_do_action(self): |
| 4286 | + if not self.account_id or not self.calendar_id: |
| 4287 | + return False |
| 4288 | + |
| 4289 | + event_id_key = self.config.get('event_id_key') |
| 4290 | + start_date_column_key = self.config.get('start_date_column_key') |
| 4291 | + end_date_column_key = self.config.get('end_date_column_key') |
| 4292 | + |
| 4293 | + if not event_id_key or not start_date_column_key or not end_date_column_key: |
| 4294 | + return False |
| 4295 | + |
| 4296 | + event_id_column = self.col_key_dict.get(event_id_key) |
| 4297 | + start_date_column = self.col_key_dict.get(start_date_column_key) |
| 4298 | + end_date_column = self.col_key_dict.get(end_date_column_key) |
| 4299 | + |
| 4300 | + if not event_id_column or event_id_column.get('type') != ColumnTypes.TEXT: |
| 4301 | + return False |
| 4302 | + if not start_date_column or start_date_column.get('type') != ColumnTypes.DATE: |
| 4303 | + return False |
| 4304 | + if not end_date_column or end_date_column.get('type') != ColumnTypes.DATE: |
| 4305 | + return False |
| 4306 | + |
| 4307 | + start_time = self._get_field_value_by_column_key(start_date_column_key) |
| 4308 | + end_time = self._get_field_value_by_column_key(end_date_column_key) |
| 4309 | + if not start_time or not end_time: |
| 4310 | + return False |
| 4311 | + return True |
| 4312 | + |
| 4313 | + def do_action(self): |
| 4314 | + if not self.can_do_action(): |
| 4315 | + return |
| 4316 | + if self.function_type == 'create_event': |
| 4317 | + self.create_event() |
| 4318 | + elif self.function_type == 'update_event': |
| 4319 | + self.update_event() |
| 4320 | + else: |
| 4321 | + auto_rule_logger.warning('google calendar action %s not supported', self.action_type) |
| 4322 | + return |
| 4323 | + |
| 4324 | + |
| 4325 | + |
4076 | 4326 | class RuleInvalidException(Exception): |
4077 | 4327 | """ |
4078 | 4328 | Exception which indicates rule need to be set is_valid=Fasle |
@@ -4446,6 +4696,10 @@ def can_condition_trigger_action(self, action): |
4446 | 4696 | if run_condition == PER_UPDATE: |
4447 | 4697 | return True |
4448 | 4698 | return False |
| 4699 | + elif action_type == 'google_calendar': |
| 4700 | + if self.run_condition == PER_UPDATE: |
| 4701 | + return True |
| 4702 | + return False |
4449 | 4703 | return False |
4450 | 4704 |
|
4451 | 4705 | def do_actions(self, db_session, with_test=False): |
@@ -4673,6 +4927,26 @@ def do_actions(self, db_session, with_test=False): |
4673 | 4927 | config = config_map.get(ai_function, {}).get('config') |
4674 | 4928 | RunAI(self, action_info.get('type'), self.data, ai_function, config).do_action() |
4675 | 4929 |
|
| 4930 | + elif action_info.get('type') == 'google_calendar': |
| 4931 | + function_type = action_info.get('action_type') |
| 4932 | + config = { |
| 4933 | + 'account_id': action_info.get('account_id'), |
| 4934 | + 'calendar_id': action_info.get('calendar_id'), |
| 4935 | + 'dont_send_notifications': action_info.get('dont_send_notifications', False), |
| 4936 | + 'start_date_column_key': action_info.get('start_date_column_key'), |
| 4937 | + 'end_date_column_key': action_info.get('end_date_column_key'), |
| 4938 | + 'event_description': action_info.get('event_description', ''), |
| 4939 | + 'event_title': action_info.get('event_title'), |
| 4940 | + 'guests_can_invite': action_info.get('guests_can_invite', False), |
| 4941 | + 'guests_can_modify': action_info.get('guests_can_modify', False), |
| 4942 | + 'guests_can_see_list': action_info.get('guests_can_see_list', False), |
| 4943 | + 'location': action_info.get('location', ''), |
| 4944 | + 'video_conferencing': action_info.get('video_conferencing', False), |
| 4945 | + 'attendees': action_info.get('attendees', []), |
| 4946 | + 'event_id_key': action_info.get('event_id_key'), |
| 4947 | + } |
| 4948 | + GoogleCalendar(self, action_info.get('type'), self.data, config, function_type).do_action() |
| 4949 | + |
4676 | 4950 | except RuleInvalidException as e: |
4677 | 4951 | auto_rule_logger.warning('auto rule %s with data %s, invalid error: %s', self.rule_id, self.data, e) |
4678 | 4952 | self.task_run_success = False |
|
0 commit comments