Skip to content
2 changes: 1 addition & 1 deletion charts/lfx-v1-sync-helper/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ apiVersion: v2
name: lfx-v1-sync-helper
description: LFX Platform v1 Sync Helper chart
type: application
version: 0.3.6
version: 0.3.7
2 changes: 2 additions & 0 deletions cmd/lfx-v1-sync-helper/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ func handleKVPut(ctx context.Context, entry jetstream.KeyValueEntry) {
handleZoomMeetingRegistrantUpdate(ctx, key, v1Data)
case "itx-zoom-meetings-mappings-v2":
handleZoomMeetingMappingUpdate(ctx, key, v1Data)
case "itx-zoom-meetings-invite-responses-v2":
handleZoomMeetingInviteResponseUpdate(ctx, key, v1Data)
case "itx-zoom-past-meetings-attendees":
handleZoomPastMeetingAttendeeUpdate(ctx, key, v1Data)
case "itx-zoom-past-meetings-invitees":
Expand Down
523 changes: 329 additions & 194 deletions cmd/lfx-v1-sync-helper/handlers_meetings.go

Large diffs are not rendered by default.

24 changes: 8 additions & 16 deletions cmd/lfx-v1-sync-helper/helper_meetings.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ func calculateOccurrences(ctx context.Context, meeting meetingInput, pastOccurre

meetingStartTime, err := time.Parse(time.RFC3339, meeting.StartTime)
if err != nil {
logger.With(errKey, err, "meeting_id", meeting.ID, "start_time", meeting.StartTime).ErrorContext(ctx, "failed to parse meeting start_time")
return nil, err
return nil, fmt.Errorf("failed to parse meeting start_time %s: %w", meeting.StartTime, err)
}

location := time.UTC
Expand Down Expand Up @@ -71,8 +70,7 @@ func calculateOccurrences(ctx context.Context, meeting meetingInput, pastOccurre
// Convert occurrence start time to timeRFC3339 format to make the time easier to read in the logs
occurrenceStartTimeUnixInt, err := strconv.ParseInt(occurrenceStartTimeUnix, 10, 64)
if err != nil {
logger.With(errKey, err, "meeting_id", meeting.ID, "occurrence_id", occurrenceStartTimeUnix, "occurrence_start_time", occurrenceStartTimeFmt).ErrorContext(ctx, "failed to parse occurrence start_time")
return nil, err
return nil, fmt.Errorf("failed to convert occurrence start time %s to int: %w", occurrenceStartTimeUnix, err)
}
occurrenceStartTimeFmt = time.Unix(occurrenceStartTimeUnixInt, 0).Format(time.RFC3339)
}
Expand Down Expand Up @@ -113,8 +111,7 @@ func calculateOccurrences(ctx context.Context, meeting meetingInput, pastOccurre
if occurrencePatternIdx < len(occurrencesPattern)-1 && occurrencesPattern[occurrencePatternIdx+1].OccurrenceID != "" {
nextRecurrenceTimeUnix, err = strconv.ParseInt(occurrencesPattern[occurrencePatternIdx+1].OccurrenceID, 10, 64)
if err != nil {
logger.With(errKey, err, "meeting_id", meeting.ID, "next_recurrence_occurrence_id", occurrencesPattern[occurrencePatternIdx+1].OccurrenceID, "next_recurrence_start_time", occurrencesPattern[occurrencePatternIdx+1].StartTime).ErrorContext(ctx, "failed to parse next recurrence start_time")
return nil, err
return nil, fmt.Errorf("failed to convert next recurrence start time %s to int: %w", occurrencesPattern[occurrencePatternIdx+1].OccurrenceID, err)
}
}
logger.With("meeting_id", meeting.ID, "current_recurrence", occurrencePattern, "next_recurrence_start_time_unix", nextRecurrenceTimeUnix, "next_recurrence_start_time", time.Unix(nextRecurrenceTimeUnix, 0).Format(time.RFC3339)).DebugContext(ctx, "next recurrence start time")
Expand All @@ -130,8 +127,7 @@ func calculateOccurrences(ctx context.Context, meeting meetingInput, pastOccurre
// Convert unix string start time into time.Time object
unixStartTime, err := strconv.ParseInt(occurrencePattern.OccurrenceID, 10, 64)
if err != nil {
logger.With(errKey, err, "meeting_id", meeting.ID, "recurrence_occurrence_id", occurrencePattern.OccurrenceID, "recurrence_start_time", occurrencePattern.StartTime).ErrorContext(ctx, "failed to parse recurrence start_time")
return nil, err
return nil, fmt.Errorf("failed to convert recurrence start time %s to int: %w", occurrencePattern.OccurrenceID, err)
}
recStartTime := time.Unix(unixStartTime, 0)
recStartTime, err = timeInLocation(recStartTime, meeting.Timezone)
Expand All @@ -142,8 +138,7 @@ func calculateOccurrences(ctx context.Context, meeting meetingInput, pastOccurre
// Get occurrences based on reccurrence pattern and start time
occurrences, err := getRRuleOccurrences(recStartTime, meeting.Timezone, occurrencePattern.Recurrence, nil)
if err != nil {
logger.With(errKey, err, "meeting_id", meeting.ID, "start_time", recStartTime, "recurrence_rrule", occurrencePattern.Recurrence).ErrorContext(ctx, "failed to get recurrence rule")
return nil, err
return nil, fmt.Errorf("failed to get recurrence rule: %w", err)
}
occurrencesInLog := occurrences
// only show the first 100 occurrences to avoid spamming the logs
Expand Down Expand Up @@ -201,8 +196,7 @@ func calculateOccurrences(ctx context.Context, meeting meetingInput, pastOccurre
currentAgenda = updatedOcc.Agenda
unixStartTime, err := strconv.ParseInt(updatedOcc.NewOccurrenceID, 10, 64)
if err != nil {
logger.With(errKey, err, "meeting_id", meeting.ID, "occurrence", updatedOcc).ErrorContext(ctx, "failed to parse updated occurrence start_time")
return nil, err
return nil, fmt.Errorf("failed to convert updated occurrence start time %s to int: %w", updatedOcc.NewOccurrenceID, err)
}
currentStartTime = time.Unix(unixStartTime, 0).In(location)
logger.With("meeting_id", meeting.ID, "current_start_time", currentStartTime, "occurrence", updatedOcc).DebugContext(ctx, "current start time changed")
Expand Down Expand Up @@ -233,8 +227,7 @@ func calculateOccurrences(ctx context.Context, meeting meetingInput, pastOccurre
// Skip past occurrences if no past occurrences are expected
unixStartTime, err := strconv.ParseInt(updatedOcc.NewOccurrenceID, 10, 64)
if err != nil {
logger.With(errKey, err, "meeting_id", meeting.ID, "occurrence_id", o.Unix(), "updated_occ", updatedOcc).ErrorContext(ctx, "failed to parse updated occurrence start_time")
return nil, err
return nil, fmt.Errorf("failed to convert updated occurrence start time %s to int: %w", updatedOcc.NewOccurrenceID, err)
}

// If updated occurrence does not have a duration, use meeting duration
Expand Down Expand Up @@ -441,8 +434,7 @@ func getRRule(reccurrence ZoomMeetingRecurrence, endTime *time.Time) (string, er
reccurrence.EndTimes = "0"
t, err := time.Parse(time.RFC3339, reccurrence.EndDateTime)
if err != nil {
logger.With(errKey, err, "recurrence", reccurrence).Error("error parsing recurrence end_date_time")
return "", err
return "", fmt.Errorf("failed to parse recurrence end_date_time %s: %w", reccurrence.EndDateTime, err)
}
rrule.WriteString(fmt.Sprintf("UNTIL=%s;", t.Format("20060102T150405Z")))
}
Expand Down
89 changes: 89 additions & 0 deletions cmd/lfx-v1-sync-helper/models_meetings.go
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,95 @@ type registrantInput struct {
UpdatedBy UpdatedBy `json:"updated_by" dynamodbav:"updated_by"`
}

// RSVPResponseType represents the type of RSVP response
type RSVPResponseType string

const (
// RSVPResponseAccepted indicates the registrant will attend
RSVPResponseAccepted RSVPResponseType = "accepted"
// RSVPResponseMaybe indicates the registrant might attend
RSVPResponseMaybe RSVPResponseType = "maybe"
// RSVPResponseDeclined indicates the registrant will not attend
RSVPResponseDeclined RSVPResponseType = "declined"
)

// RSVPScope represents the scope of an RSVP response
type RSVPScope string

const (
// RSVPScopeSingle indicates the RSVP applies to a single occurrence
RSVPScopeSingle RSVPScope = "single"
// RSVPScopeAll indicates the RSVP applies to all occurrences in the series
RSVPScopeAll RSVPScope = "all"
// RSVPScopeThisAndFollowing indicates the RSVP applies to a specific occurrence and all following ones
RSVPScopeThisAndFollowing RSVPScope = "this_and_following"
)

type inviteResponseInput struct {
// ID is the partition key of the invite response (it is a UUID)
ID string `json:"id" dynamodbav:"id"`

// MeetingAndOccurrenceID is the ID of the combined meeting and occurrence associated with the invite response
MeetingAndOccurrenceID string `json:"meeting_and_occurrence_id" dynamodbav:"meeting_and_occurrence_id"`

// MeetingID is the ID of the meeting that the invite response is associated with.
// It is a Global Secondary Index on the invite response table.
MeetingID string `json:"meeting_id" dynamodbav:"meeting_id"`

// OccurrenceID is the ID of the occurrence that the invite response is associated with.
OccurrenceID string `json:"occurrence_id" dynamodbav:"occurrence_id"`

// RegistrantID is the ID of the registrant that the invite response is associated with.
// It is a Global Secondary Index on the invite response table.
RegistrantID string `json:"registrant_id" dynamodbav:"registrant_id"`

// Email is the email of the registrant that the invite response is associated with.
// It is a Global Secondary Index on the invite response table.
Email string `json:"email" dynamodbav:"email"`

// Name is the name of the registrant that the invite response is associated with.
Name string `json:"name" dynamodbav:"name"`

// UserID is the ID of the user that the invite response is associated with.
UserID string `json:"user_id" dynamodbav:"user_id"`

// Username is the LF username of the registrant that the invite response is associated with.
// This is a v2 only attribute, meaning the username is for an LF user in the v2 system.
Username string `json:"username"`

// Org is the organization of the registrant that the invite response is associated with.
Org string `json:"org" dynamodbav:"org"`

// JobTitle is the job title of the registrant that the invite response is associated with.
JobTitle string `json:"job_title" dynamodbav:"job_title"`

// Response is the response of the registrant that the invite response is associated with.
// It is a Global Secondary Index on the invite response table.
Response RSVPResponseType `json:"response" dynamodbav:"response"`

// Scope is the scope of the response (single/all/this_and_following)
// This is only a v2 attribute.
Scope RSVPScope `json:"scope"`

// ResponseDate is the date of the invite response from the registrant.
ResponseDate string `json:"response_date" dynamodbav:"response_date"`

// SESMessageID is the SES message ID of the invite response.
SESMessageID string `json:"ses_message_id" dynamodbav:"ses_message_id"`

// EmailSubject is the subject of the invite response email.
EmailSubject string `json:"email_subject" dynamodbav:"email_subject"`

// EmailText is the text of the invite response email.
EmailText string `json:"email_text" dynamodbav:"email_text"`

// CreatedAt is the timestamp in RFC3339 format of when the invite response was created.
CreatedAt string `json:"created_at" dynamodbav:"created_at"`

// ModifiedAt is the timestamp in RFC3339 format of when the invite response was last modified.
ModifiedAt string `json:"modified_at" dynamodbav:"modified_at"`
}

// pastMeetingInput represents input data for past meeting records.
type pastMeetingInput struct {
// ID is the partition key of the past meeting table
Expand Down
2 changes: 1 addition & 1 deletion meltano/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Common environment variables that may need to be set:
- `TAP_POSTGRES_USER`: PostgreSQL username (default: `lfit`)
- `TAP_POSTGRES_DATABASE`: PostgreSQL database name (default: `sfdc`)

For additional PostgreSQL extractor settings, see the [tap-postgres documentation](https://hub.meltano.com/extractors/tap-postgres).
For additional PostgreSQL extractor settings, see the [tap-postgres documentation](https://hub.meltano.com/extractors/tap-postgres/).

#### NATS Configuration

Expand Down
1 change: 1 addition & 0 deletions meltano/meltano.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ plugins:
- itx-zoom-past-meetings-recordings
- itx-zoom-past-meetings-summaries
- itx-zoom-meetings-registrants-v2
- itx-zoom-meetings-invite-responses-v2
metadata:
"*":
# Setting the replication key allows conditional loads for
Expand Down